122 lines
4.8 KiB
TypeScript
122 lines
4.8 KiB
TypeScript
/**
|
|
* 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<string, unknown>
|
|
: {};
|
|
const details = record.inputTokenDetails && typeof record.inputTokenDetails === "object"
|
|
? record.inputTokenDetails as Record<string, unknown>
|
|
: {};
|
|
const promptDetails = record.prompt_tokens_details && typeof record.prompt_tokens_details === "object"
|
|
? record.prompt_tokens_details as Record<string, unknown>
|
|
: {};
|
|
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<string, typeof thinking> = {
|
|
openai: thinking,
|
|
};
|
|
if (providerId) providerOptions[providerId] = thinking;
|
|
return {
|
|
modelSettings: {
|
|
maxOutputTokens: agentOutputTokenBudget(thinkingMode, {
|
|
answerTokens: options.answerTokens,
|
|
thinkingTokens: options.thinkingTokens,
|
|
}),
|
|
},
|
|
providerOptions,
|
|
};
|
|
}
|