// Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import { IAiEnvironment } from "./config/env.ts"; import { IAiTool } from "./tools/tool.ts"; export type AiSdkType = "ollama" | "openai"; export interface IAiProvider { _id: string; name: string; sdk: AiSdkType; baseUrl: string; apiKey: string; } export interface IAiModelConfig { provider: IAiProvider; modelId: string; params: { reasoning: boolean | "high" | "medium" | "low"; temperature: number; topP: number; topK: number; /** Ollama: -1 = unlimited (generate until natural stop or context limit) */ numPredict: number; /** Context window size (input + output tokens); Ollama passes as num_ctx */ numCtx: number; /** OpenAI-compatible: maximum completion tokens the model can generate */ maxCompletionTokens: number; }; } export interface IAiInferenceStats { tokenCounts: { input: number; thinking: number; response: number; }; duration: { seconds: number; text: string; }; } export interface IAiGenerateOptions { prompt: string; systemPrompt?: string; signal?: AbortSignal; } export interface IAiGenerateResponse { response: string; thinking?: string; stats: IAiInferenceStats; done: boolean; doneReason?: string; } export interface IContextChatMessage { createdAt: Date; role: string; callId?: string; toolCallId?: string; toolName?: string; toolCalls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string; }; }>; content: string; user?: { _id: string; username: string; displayName: string; }; } export interface IToolCall { callId: string; function: { name: string; arguments: string; }; } export interface IToolCallResult { callId: string; functionName: string; result: string; error?: string; } export interface IAiChatOptions { systemPrompt?: string; userPrompt?: string; context?: IContextChatMessage[]; tools?: IAiTool[]; signal?: AbortSignal; } export interface IAiChatResponse { response: string; thinking?: string; stats: IAiInferenceStats; done: boolean; doneReason?: string; toolCalls?: IToolCall[]; toolCallResults?: IToolCallResult[]; } export interface IAiStreamChunk { type: "thinking" | "response" | "toolCall"; data: string; toolCallId?: string; toolName?: string; params?: string; } export type IAiResponseStreamFn = (chunk: IAiStreamChunk) => Promise; export interface IAiLogger { debug(message: string, metadata?: unknown): Promise | void; info(message: string, metadata?: unknown): Promise | void; warn(message: string, metadata?: unknown): Promise | void; error(message: string, metadata?: unknown): Promise | void; } const noOpLogger: IAiLogger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, }; export function defaultLogger(): IAiLogger { return noOpLogger; } export interface IAiModelListResult { models: Array<{ id: string; name: string; parameterLabel?: string; parameterCount?: number; contextWindow?: number; }>; } export interface IAiModelProbeResult { capabilities: { canCallTools: boolean; hasVision: boolean; hasEmbedding: boolean; hasThinking: boolean; isInstructTuned: boolean; }; settings?: { temperature?: number; topP?: number; topK?: number; numCtx?: number; /** Ollama: discovered num_predict from model parameters (informational; overridden to -1 at inference time) */ numPredict?: number; /** OpenAI-compatible: discovered from model info */ maxCompletionTokens?: number; }; } export abstract class AiApi { protected env: IAiEnvironment; protected provider: IAiProvider; protected log: IAiLogger; constructor(env: IAiEnvironment, provider: IAiProvider, logger?: IAiLogger) { this.env = env; this.provider = provider; this.log = logger ?? defaultLogger(); } abstract listModels(): Promise; abstract probeModel(modelId: string): Promise; abstract generate( model: IAiModelConfig, options: IAiGenerateOptions, streamCallback?: IAiResponseStreamFn, ): Promise; abstract chat( model: IAiModelConfig, options: IAiChatOptions, streamCallback?: IAiResponseStreamFn, ): Promise; /** * Forcefully abort any in-progress API request. * Provider-specific implementations (e.g., Ollama) should terminate * the underlying HTTP request immediately. Default is no-op. */ abort(): void { // Override in provider-specific implementations } protected assertNonEmptyChatResponse(response: IAiChatResponse): void { const hasResponse = response.response.trim().length > 0; const hasThinking = !!response.thinking?.trim(); const hasToolCalls = !!response.toolCalls?.length; const hasToolResults = !!response.toolCallResults?.length; if (!hasResponse && !hasThinking && !hasToolCalls && !hasToolResults) { throw new Error( "Provider returned an empty chat response: no text, thinking, tool calls, or tool results.", ); } } }