ktx/packages/llm/src/model-provider.ts

153 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-05-10 23:12:26 +02:00
import { createAnthropic } from '@ai-sdk/anthropic';
import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
import { createGateway, generateText, type LanguageModel } from 'ai';
2026-05-10 23:51:24 +02:00
import { createKtxToolCallRepairHandler } from './repair.js';
2026-05-10 23:12:26 +02:00
import type {
2026-05-10 23:51:24 +02:00
KtxLlmConfig,
KtxLlmProvider,
KtxModelRole,
KtxPromptCacheTtl,
KtxPromptCachingConfig,
KtxProviderOptions,
2026-05-10 23:12:26 +02:00
} from './types.js';
type AnthropicFactory = typeof createAnthropic;
type AnthropicModelFactory = (modelId: string) => LanguageModel;
type VertexAnthropicFactory = (options?: Parameters<typeof createVertexAnthropic>[0]) => AnthropicModelFactory;
type GatewayFactory = (options?: Parameters<typeof createGateway>[0]) => AnthropicModelFactory;
2026-05-10 23:51:24 +02:00
export interface KtxLlmProviderFactoryDeps {
2026-05-10 23:12:26 +02:00
createAnthropic?: (options?: Parameters<AnthropicFactory>[0]) => AnthropicModelFactory;
createVertexAnthropic?: VertexAnthropicFactory;
createGateway?: GatewayFactory;
generateText?: typeof generateText;
}
2026-05-10 23:51:24 +02:00
const DEFAULT_PROMPT_CACHING: KtxPromptCachingConfig = {
2026-05-10 23:12:26 +02:00
enabled: true,
systemTtl: '1h',
toolsTtl: '1h',
historyTtl: '5m',
cacheSystem: true,
cacheTools: true,
cacheHistory: true,
vertexFallbackTo5m: false,
};
const DIRECT_ANTHROPIC_BETA_HEADER = 'interleaved-thinking-2025-05-14,extended-cache-ttl-2025-04-11';
2026-05-10 23:51:24 +02:00
function resolvePromptCaching(config: KtxLlmConfig): KtxPromptCachingConfig {
2026-05-10 23:12:26 +02:00
return { ...DEFAULT_PROMPT_CACHING, ...config.promptCaching };
}
export function modelIdFromLanguageModel(model: LanguageModel | string): string {
return typeof model === 'string' ? model : ((model as { modelId?: string }).modelId ?? '');
}
export function isAnthropicProtocolModel(model: LanguageModel | string): boolean {
const modelId = modelIdFromLanguageModel(model);
return modelId.startsWith('claude-') || modelId.startsWith('anthropic/') || modelId.includes('/claude-');
}
2026-05-10 23:51:24 +02:00
class DefaultKtxLlmProvider implements KtxLlmProvider {
private readonly promptCaching: KtxPromptCachingConfig;
2026-05-10 23:12:26 +02:00
private readonly getModelByResolvedName: (modelId: string) => LanguageModel;
private readonly runGenerateText: typeof generateText;
constructor(
2026-05-10 23:51:24 +02:00
private readonly config: KtxLlmConfig,
deps: KtxLlmProviderFactoryDeps,
2026-05-10 23:12:26 +02:00
) {
this.promptCaching = resolvePromptCaching(config);
this.runGenerateText = deps.generateText ?? generateText;
this.getModelByResolvedName = this.createModelFactory(config, deps);
}
2026-05-10 23:51:24 +02:00
getModel(role: KtxModelRole): LanguageModel {
2026-05-10 23:12:26 +02:00
return this.getModelByName(this.resolveRole(role));
}
getModelByName(modelId: string): LanguageModel {
return this.getModelByResolvedName(modelId);
}
2026-05-10 23:51:24 +02:00
cacheMarker(ttl: KtxPromptCacheTtl, model?: LanguageModel | string) {
2026-05-10 23:12:26 +02:00
if (!this.promptCaching.enabled) {
return undefined;
}
if (model && !isAnthropicProtocolModel(model)) {
return undefined;
}
return { anthropic: { cacheControl: { type: 'ephemeral' as const, ttl } } };
}
repairToolCallHandler(options: { source?: string } = {}) {
2026-05-10 23:51:24 +02:00
return createKtxToolCallRepairHandler({
source: options.source ?? 'ktx-llm',
2026-05-10 23:12:26 +02:00
getRepairModel: () => this.getModel('repair'),
generateText: this.runGenerateText,
});
}
2026-05-10 23:51:24 +02:00
thinkingProviderOptions(_role: KtxModelRole, budgetTokens: number): KtxProviderOptions {
2026-05-10 23:12:26 +02:00
return {
anthropic: {
thinking: { type: 'enabled', budgetTokens },
},
};
}
telemetryConfig() {
return this.config.telemetry?.experimentalTelemetry;
}
2026-05-10 23:51:24 +02:00
promptCachingConfig(): KtxPromptCachingConfig {
2026-05-10 23:12:26 +02:00
return this.promptCaching;
}
activeBackend() {
return this.config.backend;
}
2026-05-10 23:51:24 +02:00
private resolveRole(role: KtxModelRole): string {
2026-05-10 23:12:26 +02:00
return this.config.modelSlots[role] ?? this.config.modelSlots.default;
}
2026-05-10 23:51:24 +02:00
private createModelFactory(config: KtxLlmConfig, deps: KtxLlmProviderFactoryDeps): (modelId: string) => LanguageModel {
2026-05-10 23:12:26 +02:00
if (config.backend === 'anthropic') {
const anthropic = (deps.createAnthropic ?? createAnthropic)({
...(config.anthropic?.apiKey ? { apiKey: config.anthropic.apiKey } : {}),
...(config.anthropic?.baseURL ? { baseURL: config.anthropic.baseURL } : {}),
headers: {
'anthropic-beta': DIRECT_ANTHROPIC_BETA_HEADER,
},
});
return (modelId) => anthropic(modelId);
}
if (config.backend === 'vertex') {
if (!config.vertex?.location) {
2026-05-10 23:51:24 +02:00
throw new Error('vertex.location is required when KTX LLM backend is vertex');
2026-05-10 23:12:26 +02:00
}
const vertex = (deps.createVertexAnthropic ?? createVertexAnthropic)({
...(config.vertex.project ? { project: config.vertex.project } : {}),
location: config.vertex.location,
});
return (modelId) => vertex(modelId);
}
const gateway = (deps.createGateway ?? createGateway)({
...(config.gateway?.apiKey ? { apiKey: config.gateway.apiKey } : {}),
...(config.gateway?.baseURL ? { baseURL: config.gateway.baseURL } : {}),
});
return (modelId) => gateway(modelId);
}
}
2026-05-10 23:51:24 +02:00
export function createKtxLlmProvider(config: KtxLlmConfig, deps: KtxLlmProviderFactoryDeps = {}): KtxLlmProvider {
2026-05-10 23:12:26 +02:00
if (!config.modelSlots.default) {
throw new Error('modelSlots.default is required');
}
2026-05-10 23:51:24 +02:00
return new DefaultKtxLlmProvider(config, deps);
2026-05-10 23:12:26 +02:00
}