import type { StreamFn } from "@earendil-works/pi-agent-core"; import { streamSimple } from "@earendil-works/pi-ai"; import { streamWithPayloadPatch } from "../agents/pi-embedded-runner/stream-payload-utils.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import type { ProviderWrapStreamFnContext } from "./plugin-entry.js"; export type ProviderStreamWrapperFactory = | ((streamFn: StreamFn | undefined) => StreamFn | undefined) | null | undefined | false; export function composeProviderStreamWrappers( baseStreamFn: StreamFn | undefined, ...wrappers: ProviderStreamWrapperFactory[] ): StreamFn | undefined { return wrappers.reduce( (streamFn, wrapper) => (wrapper ? wrapper(streamFn) : streamFn), baseStreamFn, ); } /** @deprecated Bundled provider stream helper; do not use from third-party plugins. */ export function defaultToolStreamExtraParams( extraParams?: Record, ): Record { if (extraParams?.tool_stream !== undefined) { return extraParams; } return { ...extraParams, tool_stream: true, }; } export function createPayloadPatchStreamWrapper( baseStreamFn: StreamFn | undefined, patchPayload: (params: { payload: Record; model: Parameters[0]; context: Parameters[1]; options: Parameters[2]; }) => void, wrapperOptions?: { shouldPatch?: (params: { model: Parameters[0]; context: Parameters[1]; options: Parameters[2]; }) => boolean; }, ): StreamFn { const underlying = baseStreamFn ?? streamSimple; return (model, context, options) => { if (wrapperOptions?.shouldPatch && !wrapperOptions.shouldPatch({ model, context, options })) { return underlying(model, context, options); } return streamWithPayloadPatch(underlying, model, context, options, (payload) => patchPayload({ payload, model, context, options }), ); }; } function isAnthropicThinkingEnabled(payload: Record): boolean { const thinking = payload.thinking; if (!thinking || typeof thinking !== "object") { return false; } return (thinking as { type?: unknown }).type !== "disabled"; } function assistantMessageHasAnthropicToolUse(message: Record): boolean { if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) { return true; } const content = message.content; if (!Array.isArray(content)) { return false; } return content.some( (block) => block && typeof block === "object" && ((block as { type?: unknown }).type === "tool_use" || (block as { type?: unknown }).type === "toolCall"), ); } function stripTrailingAssistantPrefillMessages(payload: Record): number { if (!Array.isArray(payload.messages)) { return 0; } let stripped = 0; while (payload.messages.length > 0) { const finalMessage = payload.messages[payload.messages.length - 1]; if (!finalMessage || typeof finalMessage !== "object") { break; } const message = finalMessage as Record; if (message.role !== "assistant" || assistantMessageHasAnthropicToolUse(message)) { break; } payload.messages.pop(); stripped += 1; } return stripped; } /** @deprecated Anthropic-family provider stream helper; do not use from third-party plugins. */ export function stripTrailingAnthropicAssistantPrefillWhenThinking( payload: Record, ): number { if (!isAnthropicThinkingEnabled(payload)) { return 0; } return stripTrailingAssistantPrefillMessages(payload); } /** @deprecated Anthropic-family provider stream helper; do not use from third-party plugins. */ export function createAnthropicThinkingPrefillPayloadWrapper( baseStreamFn: StreamFn | undefined, onStripped?: (stripped: number) => void, wrapperOptions?: Parameters[2], ): StreamFn { return createPayloadPatchStreamWrapper( baseStreamFn, ({ payload }) => { const stripped = stripTrailingAnthropicAssistantPrefillWhenThinking(payload); if (stripped > 0) { onStripped?.(stripped); } }, wrapperOptions, ); } /** @deprecated OpenAI-compatible provider stream helper; do not use from third-party plugins. */ export type OpenAICompatibleThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"]; /** @deprecated OpenAI-compatible provider stream helper; do not use from third-party plugins. */ export function isOpenAICompatibleThinkingEnabled(params: { thinkingLevel: OpenAICompatibleThinkingLevel; options: Parameters[2]; }): boolean { const options = (params.options ?? {}) as { reasoningEffort?: unknown; reasoning?: unknown }; const raw = options.reasoningEffort ?? options.reasoning ?? params.thinkingLevel ?? "high"; if (typeof raw !== "string") { return true; } const normalized = raw.trim().toLowerCase(); return normalized !== "off" && normalized !== "none"; } /** @deprecated DeepSeek provider stream helper; do not use from third-party plugins. */ export type DeepSeekV4ThinkingLevel = ProviderWrapStreamFnContext["thinkingLevel"]; /** @deprecated DeepSeek provider stream helper; do not use from third-party plugins. */ export type DeepSeekV4ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; function isDisabledDeepSeekV4ThinkingLevel(thinkingLevel: DeepSeekV4ThinkingLevel): boolean { const normalized = typeof thinkingLevel === "string" ? thinkingLevel.toLowerCase() : ""; return normalized === "off" || normalized === "none"; } function resolveDeepSeekV4ReasoningEffort( thinkingLevel: DeepSeekV4ThinkingLevel, ): DeepSeekV4ReasoningEffort { return thinkingLevel === "xhigh" || thinkingLevel === "max" ? "max" : "high"; } function stripDeepSeekV4ReasoningContent(payload: Record): void { if (!Array.isArray(payload.messages)) { return; } for (const message of payload.messages) { if (!message || typeof message !== "object") { continue; } delete (message as Record).reasoning_content; } } function ensureDeepSeekV4AssistantReasoningContent(payload: Record): void { if (!Array.isArray(payload.messages)) { return; } for (const message of payload.messages) { if (!message || typeof message !== "object") { continue; } const record = message as Record; if (record.role !== "assistant") { continue; } if (!("reasoning_content" in record)) { record.reasoning_content = ""; } } } /** @deprecated DeepSeek provider stream helper; do not use from third-party plugins. */ export function createDeepSeekV4OpenAICompatibleThinkingWrapper(params: { baseStreamFn: StreamFn | undefined; thinkingLevel: DeepSeekV4ThinkingLevel; shouldPatchModel: (model: Parameters[0]) => boolean; resolveReasoningEffort?: (thinkingLevel: DeepSeekV4ThinkingLevel) => DeepSeekV4ReasoningEffort; }): StreamFn | undefined { if (!params.baseStreamFn) { return undefined; } const underlying = params.baseStreamFn; const resolveReasoningEffort = params.resolveReasoningEffort ?? resolveDeepSeekV4ReasoningEffort; return (model, context, options) => { if (!params.shouldPatchModel(model)) { return underlying(model, context, options); } return streamWithPayloadPatch(underlying, model, context, options, (payload) => { if (isDisabledDeepSeekV4ThinkingLevel(params.thinkingLevel)) { payload.thinking = { type: "disabled" }; delete payload.reasoning_effort; delete payload.reasoning; stripDeepSeekV4ReasoningContent(payload); return; } payload.thinking = { type: "enabled" }; payload.reasoning_effort = resolveReasoningEffort(params.thinkingLevel); ensureDeepSeekV4AssistantReasoningContent(payload); }); }; } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export type GoogleThinkingInputLevel = | "off" | "minimal" | "low" | "medium" | "adaptive" | "high" | "max" | "xhigh"; // Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with // "Budget 0 is invalid. This model only works in thinking mode." /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function isGoogleThinkingRequiredModel(modelId: string): boolean { return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro"); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function isGoogleGemini25ThinkingBudgetModel(modelId: string): boolean { return /(?:^|\/)gemini-2\.5-/.test(normalizeLowercaseStringOrEmpty(modelId)); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function isGoogleGemini3ProModel(modelId: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(modelId); return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function isGoogleGemini3FlashModel(modelId: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(modelId); return /(?:^|\/)gemini-(?:3(?:\.\d+)?-flash|flash(?:-lite)?-latest)(?:-|$)/.test(normalized); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function isGoogleGemini3ThinkingLevelModel(modelId: string): boolean { return isGoogleGemini3ProModel(modelId) || isGoogleGemini3FlashModel(modelId); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function resolveGoogleGemini3ThinkingLevel(params: { modelId?: string; thinkingLevel?: GoogleThinkingInputLevel; thinkingBudget?: number; }): GoogleThinkingLevel | undefined { if (typeof params.modelId !== "string") { return undefined; } if (isGoogleGemini3ProModel(params.modelId)) { switch (params.thinkingLevel) { case "off": case "minimal": case "low": return "LOW"; case "medium": case "high": case "max": case "xhigh": return "HIGH"; case "adaptive": return undefined; case undefined: break; } if (typeof params.thinkingBudget === "number") { if (params.thinkingBudget < 0) { return undefined; } return params.thinkingBudget <= 2048 ? "LOW" : "HIGH"; } return undefined; } if (!isGoogleGemini3FlashModel(params.modelId)) { return undefined; } switch (params.thinkingLevel) { case "off": case "minimal": return "MINIMAL"; case "low": return "LOW"; case "medium": return "MEDIUM"; case "high": case "max": case "xhigh": return "HIGH"; case "adaptive": return undefined; case undefined: break; } if (typeof params.thinkingBudget !== "number") { return undefined; } if (params.thinkingBudget < 0) { return undefined; } if (params.thinkingBudget <= 0) { return "MINIMAL"; } if (params.thinkingBudget <= 2048) { return "LOW"; } if (params.thinkingBudget <= 8192) { return "MEDIUM"; } return "HIGH"; } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function stripInvalidGoogleThinkingBudget(params: { thinkingConfig: Record; modelId?: string; }): boolean { if ( params.thinkingConfig.thinkingBudget !== 0 || typeof params.modelId !== "string" || !isGoogleThinkingRequiredModel(params.modelId) ) { return false; } delete params.thinkingConfig.thinkingBudget; return true; } function isGemma4Model(modelId: string): boolean { return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); } function mapThinkLevelToGemma4ThinkingLevel( thinkingLevel?: GoogleThinkingInputLevel, ): "MINIMAL" | "HIGH" | undefined { switch (thinkingLevel) { case "off": return undefined; case "minimal": case "low": return "MINIMAL"; case "medium": case "adaptive": case "high": case "max": case "xhigh": return "HIGH"; default: return undefined; } } function normalizeGemma4ThinkingLevel(value: unknown): "MINIMAL" | "HIGH" | undefined { if (typeof value !== "string") { return undefined; } switch (value.trim().toUpperCase()) { case "MINIMAL": case "LOW": return "MINIMAL"; case "MEDIUM": case "HIGH": return "HIGH"; default: return undefined; } } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function sanitizeGoogleThinkingPayload(params: { payload: unknown; modelId?: string; thinkingLevel?: GoogleThinkingInputLevel; }): void { if (!params.payload || typeof params.payload !== "object") { return; } const payloadObj = params.payload as Record; sanitizeGoogleThinkingConfigContainer({ container: payloadObj.config, modelId: params.modelId, thinkingLevel: params.thinkingLevel, }); sanitizeGoogleThinkingConfigContainer({ container: payloadObj.generationConfig, modelId: params.modelId, thinkingLevel: params.thinkingLevel, }); } function sanitizeGoogleThinkingConfigContainer(params: { container: unknown; modelId?: string; thinkingLevel?: GoogleThinkingInputLevel; }): void { if (!params.container || typeof params.container !== "object") { return; } const configObj = params.container as Record; const thinkingConfig = configObj.thinkingConfig; if (!thinkingConfig || typeof thinkingConfig !== "object") { return; } const thinkingConfigObj = thinkingConfig as Record; if (typeof params.modelId === "string" && isGemma4Model(params.modelId)) { const normalizedThinkingLevel = normalizeGemma4ThinkingLevel(thinkingConfigObj.thinkingLevel); const explicitMappedLevel = mapThinkLevelToGemma4ThinkingLevel(params.thinkingLevel); const disabledViaBudget = typeof thinkingConfigObj.thinkingBudget === "number" && thinkingConfigObj.thinkingBudget <= 0; const hadThinkingBudget = thinkingConfigObj.thinkingBudget !== undefined; delete thinkingConfigObj.thinkingBudget; if ( params.thinkingLevel === "off" || (disabledViaBudget && explicitMappedLevel === undefined && !normalizedThinkingLevel) ) { delete thinkingConfigObj.thinkingLevel; if (Object.keys(thinkingConfigObj).length === 0) { delete configObj.thinkingConfig; } return; } const mappedLevel = explicitMappedLevel ?? normalizedThinkingLevel ?? (hadThinkingBudget ? "MINIMAL" : undefined); if (mappedLevel) { thinkingConfigObj.thinkingLevel = mappedLevel; } return; } const thinkingBudget = thinkingConfigObj.thinkingBudget; if ( params.thinkingLevel === "adaptive" && typeof params.modelId === "string" && isGoogleGemini25ThinkingBudgetModel(params.modelId) ) { delete thinkingConfigObj.thinkingLevel; thinkingConfigObj.thinkingBudget = -1; return; } if ( params.thinkingLevel === "adaptive" && typeof params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId) ) { delete thinkingConfigObj.thinkingBudget; delete thinkingConfigObj.thinkingLevel; if (Object.keys(thinkingConfigObj).length === 0) { delete configObj.thinkingConfig; } return; } if (typeof params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) { const mappedLevel = resolveGoogleGemini3ThinkingLevel({ modelId: params.modelId, thinkingLevel: params.thinkingLevel, thinkingBudget: typeof thinkingBudget === "number" ? thinkingBudget : undefined, }); delete thinkingConfigObj.thinkingBudget; if (mappedLevel) { thinkingConfigObj.thinkingLevel = mappedLevel; } if (Object.keys(thinkingConfigObj).length === 0) { delete configObj.thinkingConfig; } return; } if ( stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId }) ) { if (Object.keys(thinkingConfigObj).length === 0) { delete configObj.thinkingConfig; } return; } if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) { return; } // pi-ai can emit thinkingBudget=-1 for some Google model IDs; a negative budget // is invalid for Google-compatible backends and can lead to malformed handling. delete thinkingConfigObj.thinkingBudget; if (Object.keys(thinkingConfigObj).length === 0) { delete configObj.thinkingConfig; } } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function createGoogleThinkingPayloadWrapper( baseStreamFn: StreamFn | undefined, thinkingLevel?: GoogleThinkingInputLevel, ): StreamFn { return createPayloadPatchStreamWrapper(baseStreamFn, ({ payload, model }) => { if (model.api === "google-generative-ai") { sanitizeGoogleThinkingPayload({ payload, modelId: model.id, thinkingLevel, }); } }); } /** @deprecated Google provider-owned stream helper; do not use from third-party plugins. */ export function createGoogleThinkingStreamWrapper( ctx: ProviderWrapStreamFnContext, ): NonNullable { return createGoogleThinkingPayloadWrapper(ctx.streamFn, ctx.thinkingLevel); } export { applyAnthropicPayloadPolicyToParams, resolveAnthropicPayloadPolicy, } from "../agents/anthropic-payload-policy.js"; export { applyAnthropicEphemeralCacheControlMarkers } from "../agents/pi-embedded-runner/anthropic-cache-control-payload.js"; export { createMoonshotThinkingWrapper, resolveMoonshotThinkingType, } from "../agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.js"; export { streamWithPayloadPatch }; export { createToolStreamWrapper, createZaiToolStreamWrapper, } from "../agents/pi-embedded-runner/zai-stream-wrappers.js";