gadget/packages/ai/src/api.ts

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.",
);
}
}
}