import type { LanguageModelV4 } from '@ai-sdk/provider' import type { Effort, InfoRecord, MessageRecord, ModelRef, PlainContent, PreferencesRecord, } from '@scn-chat/lexicons' import { z } from 'zod' export type { Effort, InfoRecord, LanguageModelV4, MessageRecord, ModelRef, PlainContent, PreferencesRecord, } /** The plugin API major version this package describes. */ export const PLUGIN_API_VERSION = 1 // Providers export type Capabilities = { vision: boolean; reasoning: boolean; tools: boolean } export type ModelInfo = { id: string; name: string; capabilities: Capabilities } /** Whether earlier reasoning and provider data are sent back to this provider on later turns. */ export type ReplayPolicy = 'replay' | 'drop' export type ModelAccess = { modelId: string /** A user's own key. When absent, the provider uses its admin key. */ apiKey?: string /** A user's own endpoint, for providers that allow user endpoints. */ baseURL?: string /** A fetch the provider must use for user endpoints, guarded against private networks. */ fetch?: typeof globalThis.fetch } export interface ModelProvider { id: string name: string hasAdminKey: boolean userKeys: boolean userEndpoints?: boolean allowPrivateNetworks?: boolean replay: ReplayPolicy /** Provider options sent with every call to this provider's models, keyed by provider name. */ providerOptions?: Record> createModel(access: ModelAccess): LanguageModelV4 listModels?(access: Omit): Promise } // Tools export type JsonValue = | string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } export type ToolContext = { user: string; conversation: string; signal: AbortSignal } export interface Tool { name: string description: string inputSchema: z.ZodType run(input: Input, context: ToolContext): Promise } export type ToolDefinition = { name: string description: string inputSchema: Record } export interface ToolSource { id: string list(user: string): Promise call(user: string, name: string, input: unknown, context: ToolContext): Promise } // Ingesters export type IngestInput = { bytes: Uint8Array; mimeType: string; name?: string } export interface Ingester { id: string accepts: string[] priority?: number method: 'text' | 'ocr' ingest(file: IngestInput): Promise<{ text: string }> } // Hooks export type TurnContext = { user: string /** `info` is missing when another client created the conversation without one. */ conversation: { uri: string; info?: InfoRecord } preferences?: PreferencesRecord userMessage: { rkey: string; record: MessageRecord } reply: { rkey: string; record: MessageRecord } /** Missing when no model was chosen and there is no default, which fails the turn. */ model?: ModelRef effort?: Effort } export type BranchMessage = { rkey: string; author: string; record: MessageRecord } export type PromptValue = { instructions: string; messages: BranchMessage[] } export type FilterHooks = { 'messages:beforeModel': { value: PromptValue; context: TurnContext } 'message:afterModel': { value: PlainContent; context: TurnContext } } export type ActionHooks = { 'turn:after': TurnContext 'conversation:created': { user: string; conversation: string } 'conversation:deleted': { user: string; conversation: string } } export type HookName = keyof FilterHooks | keyof ActionHooks export type HookOrder = 'pre' | 'normal' | 'post' export type FilterHandler = ( value: FilterHooks[N]['value'], context: FilterHooks[N]['context'], ) => FilterHooks[N]['value'] | Promise export type ActionHandler = ( payload: ActionHooks[N], ) => void | Promise export type HookHandler = N extends keyof FilterHooks ? FilterHandler : N extends keyof ActionHooks ? ActionHandler : never // Context export type GenerateTextRequest = { user: string model: ModelRef system?: string prompt: string maxOutputTokens?: number /** Ignored by models without reasoning. Unset uses the provider default. */ effort?: Effort signal?: AbortSignal } export type InfoPatch = Partial> export type Logger = { debug(obj: object, msg?: string): void info(obj: object, msg?: string): void warn(obj: object, msg?: string): void error(obj: object, msg?: string): void } export interface PluginContext { providers: { register(provider: ModelProvider): void } tools: { register(tool: Tool): void } toolSources: { register(source: ToolSource): void } ingesters: { register(ingester: Ingester): void } hooks: { on(name: N, handler: HookHandler, options?: { order?: HookOrder }): void } models: { generateText(request: GenerateTextRequest): Promise<{ text: string; finishReason: string }> } conversations: { updateInfo( user: string, conversation: string, patch: InfoPatch, options?: { unlessUserTitled?: boolean }, ): Promise } userSettings(user: string): Promise logger: Logger app: Readonly<{ name: string; publicUrl: string }> onClose(fn: () => void | Promise): void } export interface Plugin { id: string name: string apiVersion: number userSettings?: z.ZodObject & z.ZodType setup(ctx: PluginContext): void | Promise } export function definePlugin( plugin: Plugin, ): Plugin { return plugin } /** Options for a provider plugin that only needs an API key. */ export const keyedProviderOptions = z .object({ apiKey: z.string().min(1).optional(), userKeys: z.boolean().optional() }) .strict() export type KeyedProviderOptions = z.infer /** Build the factory for a provider plugin only needs an API key. */ export function keyedProvider(provider: { id: string name: string replay: ReplayPolicy providerOptions?: ModelProvider['providerOptions'] create: (apiKey: string | undefined) => (modelId: string) => LanguageModelV4 }) { return (options: KeyedProviderOptions = {}) => definePlugin({ id: provider.id, name: provider.name, apiVersion: PLUGIN_API_VERSION, setup(ctx) { ctx.providers.register({ id: provider.id, name: provider.name, hasAdminKey: Boolean(options.apiKey), userKeys: options.userKeys ?? true, replay: provider.replay, ...(provider.providerOptions ? { providerOptions: provider.providerOptions } : {}), createModel: ({ modelId, apiKey }) => provider.create(apiKey ?? options.apiKey)(modelId), }) }, }) }