Files
Jyotisha/frontend/src/lib/agent-generation-settings.ts
T
2026-08-31 09:59:53 +08:00

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,
};
}