/** * Spoken-answer generation settings shared by consultation and rectification. * * DeepSeek hidden reasoning and visible content can share one `max_tokens` * cap. The spoken budget stays 16384; when provider thinking is on, the * output cap is spoken + thinking so CoT cannot pinch the user-facing reply. * Reasoning must travel on `reasoning-delta` / `thinking.delta`, never on * `answer.delta`. */ export const AGENT_ANSWER_OUTPUT_TOKENS = 16_384; export const AGENT_THINKING_OUTPUT_TOKENS = 8_192; export const AGENT_SLICE_ANSWER_OUTPUT_TOKENS = 8_192; export const AGENT_SLICE_THINKING_OUTPUT_TOKENS = 2_048; /** @deprecated Use AGENT_ANSWER_OUTPUT_TOKENS; kept as the compose-budget alias. */ export const AGENT_MAX_OUTPUT_TOKENS = AGENT_ANSWER_OUTPUT_TOKENS; export type ThinkingMode = "enabled" | "disabled"; export type ReasoningEffort = "low" | "medium" | "high"; export type PromptCacheUsage = Readonly<{ readTokens: number; writeTokens: number; noCacheTokens: number; }>; function finiteTokenCount(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0; } /** Normalize AI SDK v6, legacy Mastra, and OpenAI raw cache usage shapes. */ export function promptCacheUsage(value: unknown): PromptCacheUsage | null { const record = value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; const details = record.inputTokenDetails && typeof record.inputTokenDetails === "object" ? record.inputTokenDetails as Record : {}; const promptDetails = record.prompt_tokens_details && typeof record.prompt_tokens_details === "object" ? record.prompt_tokens_details as Record : {}; const usage = { readTokens: finiteTokenCount(details.cacheReadTokens) || finiteTokenCount(record.cachedInputTokens) || finiteTokenCount(promptDetails.cached_tokens), writeTokens: finiteTokenCount(details.cacheWriteTokens) || finiteTokenCount(record.cacheCreationInputTokens), noCacheTokens: finiteTokenCount(details.noCacheTokens), }; return usage.readTokens || usage.writeTokens || usage.noCacheTokens ? usage : null; } export function mergePromptCacheUsage(usages: readonly (PromptCacheUsage | null | undefined)[]): PromptCacheUsage | null { const total = usages.reduce<{ readTokens: number; writeTokens: number; noCacheTokens: number }>((sum, usage) => ({ readTokens: sum.readTokens + (usage?.readTokens ?? 0), writeTokens: sum.writeTokens + (usage?.writeTokens ?? 0), noCacheTokens: sum.noCacheTokens + (usage?.noCacheTokens ?? 0), }), { readTokens: 0, writeTokens: 0, noCacheTokens: 0 }); return total.readTokens || total.writeTokens || total.noCacheTokens ? total : null; } function modelProviderId(model: unknown): string | undefined { return typeof model === "string" ? model.split("/")[0] : model && typeof model === "object" && "providerId" in model && typeof model.providerId === "string" ? model.providerId : undefined; } /** Adds only Anthropic's message-level cache marker; other providers keep current behavior. */ export function cachedSystemMessage(content: string, model?: unknown) { if (modelProviderId(model) !== "anthropic") return null; return { role: "system" as const, content, providerOptions: { anthropic: { cacheControl: { type: "ephemeral" as const } } }, }; } export function agentOutputTokenBudget( thinking: ThinkingMode, options: { answerTokens?: number; thinkingTokens?: number } = {}, ): number { const answerTokens = options.answerTokens ?? AGENT_ANSWER_OUTPUT_TOKENS; if (thinking !== "enabled") return answerTokens; return answerTokens + (options.thinkingTokens ?? AGENT_THINKING_OUTPUT_TOKENS); } export function agentGenerationSettings( model?: unknown, options: { thinking?: ThinkingMode; answerTokens?: number; thinkingTokens?: number; reasoningEffort?: ReasoningEffort; } = {}, ) { const thinkingMode: ThinkingMode = options.thinking ?? "disabled"; const thinking = { thinking: { type: thinkingMode, ...(thinkingMode === "enabled" && options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}), }, }; const providerId = typeof model === "string" ? model : model && typeof model === "object" && "providerId" in model && typeof model.providerId === "string" ? model.providerId : undefined; const providerOptions: Record = { openai: thinking, }; if (providerId) providerOptions[providerId] = thinking; return { modelSettings: { maxOutputTokens: agentOutputTokenBudget(thinkingMode, { answerTokens: options.answerTokens, thinkingTokens: options.thinkingTokens, }), }, providerOptions, }; }