import type { LanguageModel, ModelMessage, SystemModelMessage, ToolSet } from 'ai'; import { isAnthropicProtocolModel } from './model-provider.js'; import type { KtxLlmProvider, KtxPromptCacheTtl, KtxPromptParts } from './types.js'; export interface KtxSplitSystemMessagesResult { system: SystemModelMessage | SystemModelMessage[] | undefined; messages: ModelMessage[]; } export function splitKtxSystemMessages(messages: readonly ModelMessage[]): KtxSplitSystemMessagesResult { const systemMessages: SystemModelMessage[] = []; const otherMessages: ModelMessage[] = []; for (const message of messages) { if (message.role === 'system') { systemMessages.push(message); } else { otherMessages.push(message); } } return { system: systemMessages.length === 0 ? undefined : systemMessages.length === 1 ? systemMessages[0] : systemMessages, messages: otherMessages, }; } type ToolMap = ToolSet | Record>; interface KtxMessageBuilderOptions { cacheSystem?: boolean; cacheTools?: boolean; cacheLastHistory?: boolean; } interface KtxBuildInput { parts: KtxPromptParts; history: ModelMessage[]; currentMessage: ModelMessage; tools: ToolMap; model: LanguageModel | string; } interface KtxWrapSimpleInput { system?: string; messages?: ModelMessage[]; tools?: ToolMap; model: LanguageModel | string; } interface KtxBuildOutput { messages: ModelMessage[]; tools: ToolMap; } export class KtxMessageBuilder { constructor( private readonly provider: KtxLlmProvider, private readonly options: KtxMessageBuilderOptions = {}, ) {} build(input: KtxBuildInput): KtxBuildOutput { const cfg = this.provider.promptCachingConfig(); const cachingActive = cfg.enabled && isAnthropicProtocolModel(input.model); const ttls = this.resolveTtls(input.model); const messages: ModelMessage[] = []; const systemMessage: ModelMessage & { providerOptions?: unknown } = { role: 'system', content: input.parts.staticSystem, }; if (cachingActive && this.cacheSystemEnabled()) { systemMessage.providerOptions = this.provider.cacheMarker(ttls.systemTtl, input.model); } messages.push(systemMessage); if (input.parts.dynamicSystem) { messages.push({ role: 'system', content: input.parts.dynamicSystem }); } const historyToEmit = cachingActive && this.cacheHistoryEnabled() ? this.markLastHistoryMessage(input.history, ttls.historyTtl, input.model) : input.history; messages.push(...historyToEmit); messages.push(this.wrapLeading(input.currentMessage, input.parts.leadingUserContext)); return { messages, tools: this.sortAndMarkTools(input.tools, cachingActive, this.cacheToolsEnabled(), ttls.toolsTtl, input.model), }; } wrapSimple(input: KtxWrapSimpleInput): KtxBuildOutput { const cfg = this.provider.promptCachingConfig(); const cachingActive = cfg.enabled && isAnthropicProtocolModel(input.model); const ttls = this.resolveTtls(input.model); const messages: ModelMessage[] = []; if (input.system) { const systemMessage: ModelMessage & { providerOptions?: unknown } = { role: 'system', content: input.system, }; if (cachingActive && this.cacheSystemEnabled()) { systemMessage.providerOptions = this.provider.cacheMarker(ttls.systemTtl, input.model); } messages.push(systemMessage); } if (input.messages) { messages.push( ...(cachingActive && this.cacheHistoryEnabled() ? this.markLastHistoryMessage(input.messages, ttls.historyTtl, input.model) : input.messages), ); } return { messages, tools: this.sortAndMarkTools(input.tools ?? {}, cachingActive, this.cacheToolsEnabled(), ttls.toolsTtl, input.model), }; } private cacheSystemEnabled(): boolean { return this.options.cacheSystem ?? this.provider.promptCachingConfig().cacheSystem; } private cacheToolsEnabled(): boolean { return this.options.cacheTools ?? this.provider.promptCachingConfig().cacheTools; } private cacheHistoryEnabled(): boolean { return this.options.cacheLastHistory ?? this.provider.promptCachingConfig().cacheHistory; } private resolveTtls(model: LanguageModel | string): { systemTtl: KtxPromptCacheTtl; toolsTtl: KtxPromptCacheTtl; historyTtl: KtxPromptCacheTtl; } { const cfg = this.provider.promptCachingConfig(); if (cfg.vertexFallbackTo5m && this.provider.activeBackend() === 'vertex' && isAnthropicProtocolModel(model)) { return { systemTtl: '5m', toolsTtl: '5m', historyTtl: '5m' }; } return { systemTtl: cfg.systemTtl, toolsTtl: cfg.toolsTtl, historyTtl: cfg.historyTtl }; } private wrapLeading(currentMessage: ModelMessage, leadingUserContext?: string): ModelMessage { if (!leadingUserContext) { return currentMessage; } const reminderPart = { type: 'text' as const, text: `\n${leadingUserContext}\n`, }; if (typeof currentMessage.content === 'string') { return { ...currentMessage, content: [reminderPart, { type: 'text' as const, text: currentMessage.content }], } as ModelMessage; } if (Array.isArray(currentMessage.content)) { return { ...currentMessage, content: [reminderPart, ...currentMessage.content] } as ModelMessage; } return currentMessage; } private markLastHistoryMessage( history: ModelMessage[], ttl: KtxPromptCacheTtl, model: LanguageModel | string, ): ModelMessage[] { if (history.length === 0) { return history; } const out = [...history]; const last = out[out.length - 1]; const marker = this.provider.cacheMarker(ttl, model); if (!marker) { return history; } if (typeof last.content === 'string') { out[out.length - 1] = { ...last, content: [{ type: 'text', text: last.content, providerOptions: marker }], } as ModelMessage; return out; } if (Array.isArray(last.content) && last.content.length > 0) { const parts = [...last.content]; const lastPart = parts[parts.length - 1]; parts[parts.length - 1] = Object.assign({}, lastPart, { providerOptions: marker }); out[out.length - 1] = { ...last, content: parts } as ModelMessage; } return out; } private sortAndMarkTools( tools: ToolMap, cachingActive: boolean, cacheTools: boolean, ttl: KtxPromptCacheTtl, model: LanguageModel | string, ): ToolMap { const keys = Object.keys(tools).sort(); const sorted: Record = {}; for (const key of keys) { sorted[key] = tools[key as keyof typeof tools]; } if (cachingActive && cacheTools && keys.length > 0) { const lastKey = keys[keys.length - 1]; const marker = this.provider.cacheMarker(ttl, model); if (marker) { sorted[lastKey] = { ...(sorted[lastKey] as Record), providerOptions: marker }; } } return sorted as ToolMap; } }