222 lines
5.2 KiB
TypeScript
222 lines
5.2 KiB
TypeScript
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
|
// 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<void>;
|
|
|
|
export interface IAiLogger {
|
|
debug(message: string, metadata?: unknown): Promise<void> | void;
|
|
info(message: string, metadata?: unknown): Promise<void> | void;
|
|
warn(message: string, metadata?: unknown): Promise<void> | void;
|
|
error(message: string, metadata?: unknown): Promise<void> | 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<IAiModelListResult>;
|
|
abstract probeModel(modelId: string): Promise<IAiModelProbeResult>;
|
|
|
|
abstract generate(
|
|
model: IAiModelConfig,
|
|
options: IAiGenerateOptions,
|
|
streamCallback?: IAiResponseStreamFn,
|
|
): Promise<IAiGenerateResponse>;
|
|
|
|
abstract chat(
|
|
model: IAiModelConfig,
|
|
options: IAiChatOptions,
|
|
streamCallback?: IAiResponseStreamFn,
|
|
): Promise<IAiChatResponse>;
|
|
|
|
/**
|
|
* 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.",
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|