@linxiraos/pi-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5066 -0
- package/README.md +1195 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
- package/dist/types/auth-broker/client.d.ts +93 -0
- package/dist/types/auth-broker/discover.d.ts +39 -0
- package/dist/types/auth-broker/index.d.ts +7 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +136 -0
- package/dist/types/auth-broker/server.d.ts +48 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +152 -0
- package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
- package/dist/types/auth-gateway/http.d.ts +56 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +36 -0
- package/dist/types/auth-gateway/types.d.ts +127 -0
- package/dist/types/auth-retry.d.ts +150 -0
- package/dist/types/auth-storage.d.ts +1258 -0
- package/dist/types/dialect/anthropic.d.ts +15 -0
- package/dist/types/dialect/catalog.d.ts +3 -0
- package/dist/types/dialect/coercion.d.ts +23 -0
- package/dist/types/dialect/deepseek.d.ts +14 -0
- package/dist/types/dialect/demotion.d.ts +30 -0
- package/dist/types/dialect/examples.d.ts +18 -0
- package/dist/types/dialect/factory.d.ts +3 -0
- package/dist/types/dialect/fenced-thinking.d.ts +53 -0
- package/dist/types/dialect/gemini.d.ts +17 -0
- package/dist/types/dialect/gemma.d.ts +15 -0
- package/dist/types/dialect/glm.d.ts +9 -0
- package/dist/types/dialect/harmony.d.ts +8 -0
- package/dist/types/dialect/hermes.d.ts +9 -0
- package/dist/types/dialect/history.d.ts +3 -0
- package/dist/types/dialect/index.d.ts +11 -0
- package/dist/types/dialect/inventory.d.ts +9 -0
- package/dist/types/dialect/kimi.d.ts +14 -0
- package/dist/types/dialect/minimax.d.ts +3 -0
- package/dist/types/dialect/owned-stream.d.ts +4 -0
- package/dist/types/dialect/qwen3.d.ts +9 -0
- package/dist/types/dialect/rendering.d.ts +54 -0
- package/dist/types/dialect/thinking.d.ts +6 -0
- package/dist/types/dialect/types.d.ts +68 -0
- package/dist/types/dialect/xml.d.ts +9 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +20 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +27 -0
- package/dist/types/error/classes.d.ts +106 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +94 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +14 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/error/rate-limit.d.ts +80 -0
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/providers/amazon-bedrock.d.ts +39 -0
- package/dist/types/providers/anthropic-client.d.ts +106 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic-wire.d.ts +345 -0
- package/dist/types/providers/anthropic.d.ts +261 -0
- package/dist/types/providers/aws-credentials.d.ts +48 -0
- package/dist/types/providers/aws-eventstream.d.ts +39 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +16 -0
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
- package/dist/types/providers/cowork-fetch.d.ts +3 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor-pi-args.d.ts +105 -0
- package/dist/types/providers/cursor.d.ts +221 -0
- package/dist/types/providers/devin.d.ts +12 -0
- package/dist/types/providers/error-message.d.ts +25 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +120 -0
- package/dist/types/providers/google-shared.d.ts +203 -0
- package/dist/types/providers/google-types.d.ts +155 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +179 -0
- package/dist/types/providers/ollama.d.ts +8 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-chat-wire.d.ts +669 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
- package/dist/types/providers/openai-codex-responses.d.ts +238 -0
- package/dist/types/providers/openai-completions.d.ts +48 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
- package/dist/types/providers/openai-responses.d.ts +131 -0
- package/dist/types/providers/openai-shared.d.ts +624 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +69 -0
- package/dist/types/providers/register-builtins.d.ts +37 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +32 -0
- package/dist/types/providers/vision-guard.d.ts +20 -0
- package/dist/types/registry/aiand.d.ts +7 -0
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
- package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/registry/api-key-login.d.ts +42 -0
- package/dist/types/registry/api-key-validation.d.ts +43 -0
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/azure.d.ts +4 -0
- package/dist/types/registry/baseten.d.ts +7 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/cerebras.d.ts +7 -0
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
- package/dist/types/registry/coreweave.d.ts +7 -0
- package/dist/types/registry/cursor.d.ts +7 -0
- package/dist/types/registry/deepseek.d.ts +8 -0
- package/dist/types/registry/derived.d.ts +5 -0
- package/dist/types/registry/devin.d.ts +8 -0
- package/dist/types/registry/exa.d.ts +8 -0
- package/dist/types/registry/firepass.d.ts +16 -0
- package/dist/types/registry/fireworks.d.ts +7 -0
- package/dist/types/registry/github-copilot.d.ts +7 -0
- package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
- package/dist/types/registry/gitlab-duo.d.ts +9 -0
- package/dist/types/registry/gmi-cloud.d.ts +7 -0
- package/dist/types/registry/google-antigravity.d.ts +9 -0
- package/dist/types/registry/google-gemini-cli.d.ts +9 -0
- package/dist/types/registry/google-vertex.d.ts +5 -0
- package/dist/types/registry/google.d.ts +4 -0
- package/dist/types/registry/groq.d.ts +4 -0
- package/dist/types/registry/huggingface.d.ts +7 -0
- package/dist/types/registry/index.d.ts +4 -0
- package/dist/types/registry/kagi.d.ts +14 -0
- package/dist/types/registry/kilo.d.ts +7 -0
- package/dist/types/registry/kimi-code.d.ts +7 -0
- package/dist/types/registry/litellm.d.ts +13 -0
- package/dist/types/registry/llama-cpp.d.ts +8 -0
- package/dist/types/registry/lm-studio.d.ts +8 -0
- package/dist/types/registry/meta.d.ts +7 -0
- package/dist/types/registry/minimax-code-cn.d.ts +6 -0
- package/dist/types/registry/minimax-code.d.ts +6 -0
- package/dist/types/registry/minimax.d.ts +4 -0
- package/dist/types/registry/mistral.d.ts +4 -0
- package/dist/types/registry/moonshot.d.ts +7 -0
- package/dist/types/registry/nanogpt.d.ts +7 -0
- package/dist/types/registry/novita.d.ts +6 -0
- package/dist/types/registry/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
- package/dist/types/registry/oauth/anthropic.d.ts +24 -0
- package/dist/types/registry/oauth/callback-server.d.ts +74 -0
- package/dist/types/registry/oauth/cursor.d.ts +16 -0
- package/dist/types/registry/oauth/device-code.d.ts +25 -0
- package/dist/types/registry/oauth/devin.d.ts +5 -0
- package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
- package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
- package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
- package/dist/types/registry/oauth/index.d.ts +45 -0
- package/dist/types/registry/oauth/kimi.d.ts +21 -0
- package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
- package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
- package/dist/types/registry/oauth/opencode.d.ts +18 -0
- package/dist/types/registry/oauth/perplexity.d.ts +9 -0
- package/dist/types/registry/oauth/pkce.d.ts +8 -0
- package/dist/types/registry/oauth/types.d.ts +87 -0
- package/dist/types/registry/oauth/wafer.d.ts +1 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
- package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
- package/dist/types/registry/oauth/zai.d.ts +25 -0
- package/dist/types/registry/ollama-cloud.d.ts +7 -0
- package/dist/types/registry/ollama.d.ts +12 -0
- package/dist/types/registry/openai-codex-device.d.ts +8 -0
- package/dist/types/registry/openai-codex.d.ts +9 -0
- package/dist/types/registry/openai.d.ts +4 -0
- package/dist/types/registry/opencode-go.d.ts +6 -0
- package/dist/types/registry/opencode-zen.d.ts +6 -0
- package/dist/types/registry/openrouter.d.ts +13 -0
- package/dist/types/registry/parallel.d.ts +14 -0
- package/dist/types/registry/perplexity.d.ts +7 -0
- package/dist/types/registry/qianfan.d.ts +7 -0
- package/dist/types/registry/qwen-portal.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +367 -0
- package/dist/types/registry/sakana.d.ts +7 -0
- package/dist/types/registry/siliconflow-cn.d.ts +7 -0
- package/dist/types/registry/siliconflow.d.ts +7 -0
- package/dist/types/registry/synthetic.d.ts +6 -0
- package/dist/types/registry/tavily.d.ts +14 -0
- package/dist/types/registry/together.d.ts +6 -0
- package/dist/types/registry/types.d.ts +75 -0
- package/dist/types/registry/umans.d.ts +7 -0
- package/dist/types/registry/venice.d.ts +13 -0
- package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
- package/dist/types/registry/vllm.d.ts +7 -0
- package/dist/types/registry/wafer-serverless.d.ts +6 -0
- package/dist/types/registry/xai-oauth.d.ts +7 -0
- package/dist/types/registry/xai.d.ts +7 -0
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
- package/dist/types/registry/xiaomi.d.ts +6 -0
- package/dist/types/registry/zai.d.ts +15 -0
- package/dist/types/registry/zenmux.d.ts +7 -0
- package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
- package/dist/types/stream.d.ts +46 -0
- package/dist/types/types.d.ts +1064 -0
- package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
- package/dist/types/usage/claude.d.ts +4 -0
- package/dist/types/usage/cursor.d.ts +4 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +15 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +3 -0
- package/dist/types/usage/ollama.d.ts +5 -0
- package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
- package/dist/types/usage/openai-codex-reset.d.ts +88 -0
- package/dist/types/usage/openai-codex.d.ts +10 -0
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/synthetic.d.ts +2 -0
- package/dist/types/usage/umans.d.ts +2 -0
- package/dist/types/usage/xai-oauth.d.ts +12 -0
- package/dist/types/usage/zai.d.ts +3 -0
- package/dist/types/usage.d.ts +527 -0
- package/dist/types/utils/abort.d.ts +25 -0
- package/dist/types/utils/anthropic-auth.d.ts +35 -0
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/block-symbols.d.ts +62 -0
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/empty-completion-retry.d.ts +21 -0
- package/dist/types/utils/event-stream.d.ts +39 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/google-validation.d.ts +2 -0
- package/dist/types/utils/harmony-leak.d.ts +135 -0
- package/dist/types/utils/http-inspector.d.ts +49 -0
- package/dist/types/utils/idle-iterator.d.ts +149 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
- package/dist/types/utils/openai-http.d.ts +48 -0
- package/dist/types/utils/openrouter-headers.d.ts +1 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/proxy.d.ts +39 -0
- package/dist/types/utils/request-debug.d.ts +29 -0
- package/dist/types/utils/retry-after.d.ts +4 -0
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +54 -0
- package/dist/types/utils/schema/index.d.ts +14 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +153 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +17 -0
- package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/typescript.d.ts +24 -0
- package/dist/types/utils/schema/wire.d.ts +52 -0
- package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
- package/dist/types/utils/sse-debug.d.ts +5 -0
- package/dist/types/utils/stream-markup-healing.d.ts +87 -0
- package/dist/types/utils/thinking-loop.d.ts +102 -0
- package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
- package/dist/types/utils/tool-choice.d.ts +52 -0
- package/dist/types/utils/validation.d.ts +28 -0
- package/dist/types/utils.d.ts +57 -0
- package/package.json +138 -0
- package/src/api-registry.ts +109 -0
- package/src/auth/sqlite-credential-store.ts +2066 -0
- package/src/auth-broker/client.ts +471 -0
- package/src/auth-broker/discover.ts +310 -0
- package/src/auth-broker/index.ts +7 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +1332 -0
- package/src/auth-broker/server.ts +898 -0
- package/src/auth-broker/snapshot-cache.ts +200 -0
- package/src/auth-broker/types.ts +193 -0
- package/src/auth-broker/wire-schema-resource.ts +487 -0
- package/src/auth-broker/wire-schemas.ts +43 -0
- package/src/auth-gateway/http.ts +227 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +836 -0
- package/src/auth-gateway/types.ts +153 -0
- package/src/auth-retry.ts +401 -0
- package/src/auth-storage.ts +6540 -0
- package/src/dialect/anthropic.md +31 -0
- package/src/dialect/anthropic.ts +608 -0
- package/src/dialect/catalog.ts +29 -0
- package/src/dialect/coercion.ts +136 -0
- package/src/dialect/deepseek.md +24 -0
- package/src/dialect/deepseek.ts +609 -0
- package/src/dialect/demotion.ts +40 -0
- package/src/dialect/examples.ts +71 -0
- package/src/dialect/factory.ts +34 -0
- package/src/dialect/fenced-thinking.ts +184 -0
- package/src/dialect/gemini.md +44 -0
- package/src/dialect/gemini.ts +583 -0
- package/src/dialect/gemma.md +33 -0
- package/src/dialect/gemma.ts +387 -0
- package/src/dialect/glm.md +32 -0
- package/src/dialect/glm.ts +579 -0
- package/src/dialect/harmony.md +31 -0
- package/src/dialect/harmony.ts +345 -0
- package/src/dialect/hermes.md +25 -0
- package/src/dialect/hermes.ts +206 -0
- package/src/dialect/history.ts +81 -0
- package/src/dialect/index.ts +15 -0
- package/src/dialect/inventory.ts +30 -0
- package/src/dialect/kimi.md +24 -0
- package/src/dialect/kimi.ts +340 -0
- package/src/dialect/minimax.md +31 -0
- package/src/dialect/minimax.ts +95 -0
- package/src/dialect/owned-stream.ts +481 -0
- package/src/dialect/prompt-template.md +12 -0
- package/src/dialect/qwen3.md +28 -0
- package/src/dialect/qwen3.ts +240 -0
- package/src/dialect/rendering.ts +304 -0
- package/src/dialect/thinking.ts +292 -0
- package/src/dialect/types.ts +56 -0
- package/src/dialect/xml.md +22 -0
- package/src/dialect/xml.ts +90 -0
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +47 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +35 -0
- package/src/error/classes.ts +281 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +602 -0
- package/src/error/format.ts +45 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +14 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +63 -0
- package/src/error/rate-limit.ts +303 -0
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +51 -0
- package/src/provider-details.ts +90 -0
- package/src/providers/amazon-bedrock.ts +1064 -0
- package/src/providers/anthropic-client.ts +317 -0
- package/src/providers/anthropic-messages-server-schema.ts +252 -0
- package/src/providers/anthropic-messages-server.ts +818 -0
- package/src/providers/anthropic-wire.ts +359 -0
- package/src/providers/anthropic.ts +4539 -0
- package/src/providers/aws-credentials.ts +772 -0
- package/src/providers/aws-eventstream.ts +181 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +438 -0
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/claude-code-fingerprint.ts +20 -0
- package/src/providers/cowork-fetch.ts +201 -0
- package/src/providers/cursor/exec-modern.ts +496 -0
- package/src/providers/cursor/proto/agent.proto +4533 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor-pi-args.ts +165 -0
- package/src/providers/cursor.ts +4689 -0
- package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
- package/src/providers/devin/proto/buf.gen.yaml +33 -0
- package/src/providers/devin/proto/buf.yaml +17 -0
- package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
- package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
- package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
- package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
- package/src/providers/devin/proto/cel/expr/value.proto +41 -0
- package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
- package/src/providers/devin/proto/errorspb/errors.proto +56 -0
- package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
- package/src/providers/devin/proto/errorspb/markers.proto +10 -0
- package/src/providers/devin/proto/errorspb/tags.proto +12 -0
- package/src/providers/devin/proto/errorspb/testing.proto +6 -0
- package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
- package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
- package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
- package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
- package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
- package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
- package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
- package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
- package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
- package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
- package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
- package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
- package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
- package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
- package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
- package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
- package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
- package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
- package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
- package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
- package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
- package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
- package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
- package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
- package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
- package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
- package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
- package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
- package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
- package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
- package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
- package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
- package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
- package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
- package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
- package/src/providers/devin.ts +679 -0
- package/src/providers/error-message.ts +23 -0
- package/src/providers/github-copilot-headers.ts +141 -0
- package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
- package/src/providers/gitlab-duo-workflow.ts +3135 -0
- package/src/providers/gitlab-duo.ts +399 -0
- package/src/providers/google-auth.ts +330 -0
- package/src/providers/google-gemini-cli.ts +1370 -0
- package/src/providers/google-shared.ts +1122 -0
- package/src/providers/google-types.ts +180 -0
- package/src/providers/google-vertex.ts +135 -0
- package/src/providers/google.ts +47 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +51 -0
- package/src/providers/mock.ts +514 -0
- package/src/providers/ollama.ts +776 -0
- package/src/providers/openai-anthropic-shim.ts +166 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +752 -0
- package/src/providers/openai-chat-wire.ts +859 -0
- package/src/providers/openai-codex/request-transformer.ts +491 -0
- package/src/providers/openai-codex/response-handler.ts +102 -0
- package/src/providers/openai-codex-responses.ts +4716 -0
- package/src/providers/openai-completions.ts +2389 -0
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses-server-schema.ts +397 -0
- package/src/providers/openai-responses-server.ts +1466 -0
- package/src/providers/openai-responses-wire.ts +6416 -0
- package/src/providers/openai-responses.ts +1393 -0
- package/src/providers/openai-shared.ts +3500 -0
- package/src/providers/pi-native-client.ts +275 -0
- package/src/providers/pi-native-server.ts +245 -0
- package/src/providers/register-builtins.ts +503 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +1083 -0
- package/src/providers/vision-guard.ts +54 -0
- package/src/registry/aiand.ts +22 -0
- package/src/registry/aimlapi.ts +6 -0
- package/src/registry/alibaba-coding-plan.ts +104 -0
- package/src/registry/alibaba-token-plan.ts +125 -0
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/registry/api-key-login.ts +115 -0
- package/src/registry/api-key-validation.ts +145 -0
- package/src/registry/aws.ts +57 -0
- package/src/registry/azure.ts +6 -0
- package/src/registry/baseten.ts +22 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/cerebras.ts +23 -0
- package/src/registry/cloudflare-ai-gateway.ts +45 -0
- package/src/registry/coreweave.ts +40 -0
- package/src/registry/cursor.ts +20 -0
- package/src/registry/deepseek.ts +46 -0
- package/src/registry/derived.ts +9 -0
- package/src/registry/devin.ts +15 -0
- package/src/registry/exa.ts +19 -0
- package/src/registry/firepass.ts +32 -0
- package/src/registry/fireworks.ts +28 -0
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo-workflow.ts +20 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/gmi-cloud.ts +22 -0
- package/src/registry/google-antigravity.ts +22 -0
- package/src/registry/google-gemini-cli.ts +22 -0
- package/src/registry/google-vertex.ts +38 -0
- package/src/registry/google.ts +6 -0
- package/src/registry/groq.ts +6 -0
- package/src/registry/huggingface.ts +29 -0
- package/src/registry/index.ts +4 -0
- package/src/registry/kagi.ts +46 -0
- package/src/registry/kilo.ts +114 -0
- package/src/registry/kimi-code.ts +17 -0
- package/src/registry/litellm.ts +45 -0
- package/src/registry/llama-cpp.ts +35 -0
- package/src/registry/lm-studio.ts +31 -0
- package/src/registry/meta.ts +22 -0
- package/src/registry/minimax-code-cn.ts +12 -0
- package/src/registry/minimax-code.ts +12 -0
- package/src/registry/minimax.ts +6 -0
- package/src/registry/mistral.ts +6 -0
- package/src/registry/moonshot.ts +28 -0
- package/src/registry/nanogpt.ts +22 -0
- package/src/registry/novita.ts +25 -0
- package/src/registry/nvidia.ts +61 -0
- package/src/registry/oauth/anthropic-constants.ts +12 -0
- package/src/registry/oauth/anthropic.ts +346 -0
- package/src/registry/oauth/callback-server.ts +438 -0
- package/src/registry/oauth/cursor.ts +187 -0
- package/src/registry/oauth/device-code.ts +92 -0
- package/src/registry/oauth/devin.ts +124 -0
- package/src/registry/oauth/github-copilot.ts +369 -0
- package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
- package/src/registry/oauth/gitlab-duo.ts +222 -0
- package/src/registry/oauth/google-antigravity.ts +225 -0
- package/src/registry/oauth/google-gemini-cli.ts +297 -0
- package/src/registry/oauth/google-oauth-shared.ts +211 -0
- package/src/registry/oauth/index.ts +187 -0
- package/src/registry/oauth/kimi.ts +297 -0
- package/src/registry/oauth/minimax-code.ts +53 -0
- package/src/registry/oauth/oauth.html +317 -0
- package/src/registry/oauth/openai-codex.ts +384 -0
- package/src/registry/oauth/opencode.ts +50 -0
- package/src/registry/oauth/perplexity.ts +228 -0
- package/src/registry/oauth/pkce.ts +18 -0
- package/src/registry/oauth/types.ts +96 -0
- package/src/registry/oauth/wafer.ts +24 -0
- package/src/registry/oauth/xai-oauth.ts +559 -0
- package/src/registry/oauth/xiaomi.ts +211 -0
- package/src/registry/oauth/zai.ts +285 -0
- package/src/registry/ollama-cloud.ts +36 -0
- package/src/registry/ollama.ts +43 -0
- package/src/registry/openai-codex-device.ts +18 -0
- package/src/registry/openai-codex.ts +19 -0
- package/src/registry/openai.ts +6 -0
- package/src/registry/opencode-go.ts +12 -0
- package/src/registry/opencode-zen.ts +12 -0
- package/src/registry/openrouter.ts +28 -0
- package/src/registry/parallel.ts +45 -0
- package/src/registry/perplexity.ts +13 -0
- package/src/registry/qianfan.ts +27 -0
- package/src/registry/qwen-portal.ts +50 -0
- package/src/registry/registry.ts +182 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/siliconflow-cn.ts +22 -0
- package/src/registry/siliconflow.ts +22 -0
- package/src/registry/synthetic.ts +21 -0
- package/src/registry/tavily.ts +45 -0
- package/src/registry/together.ts +22 -0
- package/src/registry/types.ts +86 -0
- package/src/registry/umans.ts +23 -0
- package/src/registry/venice.ts +33 -0
- package/src/registry/vercel-ai-gateway.ts +38 -0
- package/src/registry/vllm.ts +34 -0
- package/src/registry/wafer-serverless.ts +12 -0
- package/src/registry/xai-oauth.ts +17 -0
- package/src/registry/xai.ts +22 -0
- package/src/registry/xiaomi-token-plan-ams.ts +12 -0
- package/src/registry/xiaomi-token-plan-cn.ts +12 -0
- package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
- package/src/registry/xiaomi.ts +12 -0
- package/src/registry/zai.ts +41 -0
- package/src/registry/zenmux.ts +22 -0
- package/src/registry/zhipu-coding-plan.ts +27 -0
- package/src/stream.ts +1944 -0
- package/src/types.ts +1243 -0
- package/src/usage/alibaba-token-plan.ts +230 -0
- package/src/usage/claude.ts +830 -0
- package/src/usage/cursor.ts +335 -0
- package/src/usage/gemini.ts +258 -0
- package/src/usage/github-copilot.ts +424 -0
- package/src/usage/google-antigravity.ts +497 -0
- package/src/usage/kimi.ts +277 -0
- package/src/usage/minimax-code.ts +291 -0
- package/src/usage/ollama.ts +41 -0
- package/src/usage/openai-codex-base-url.ts +35 -0
- package/src/usage/openai-codex-reset.ts +205 -0
- package/src/usage/openai-codex.ts +627 -0
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/synthetic.ts +180 -0
- package/src/usage/umans.ts +192 -0
- package/src/usage/xai-oauth.ts +414 -0
- package/src/usage/zai.ts +370 -0
- package/src/usage.ts +411 -0
- package/src/utils/abort.ts +67 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/block-symbols.ts +78 -0
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/empty-completion-retry.ts +161 -0
- package/src/utils/event-stream.ts +202 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/google-validation.ts +25 -0
- package/src/utils/harmony-leak.ts +500 -0
- package/src/utils/http-inspector.ts +196 -0
- package/src/utils/idle-iterator.ts +531 -0
- package/src/utils/leaked-thinking-stream.ts +483 -0
- package/src/utils/openai-http.ts +119 -0
- package/src/utils/openrouter-headers.ts +12 -0
- package/src/utils/parse-bind.ts +56 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/proxy.ts +314 -0
- package/src/utils/request-debug.ts +351 -0
- package/src/utils/retry-after.ts +121 -0
- package/src/utils/retry.ts +77 -0
- package/src/utils/schema/CONSTRAINTS.md +168 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +210 -0
- package/src/utils/schema/index.ts +14 -0
- package/src/utils/schema/json-schema-validator.ts +595 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +2314 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +109 -0
- package/src/utils/schema/strict-tool-validation.ts +117 -0
- package/src/utils/schema/types.ts +10 -0
- package/src/utils/schema/typescript.ts +212 -0
- package/src/utils/schema/wire.ts +662 -0
- package/src/utils/sdk-stream-timeout.ts +43 -0
- package/src/utils/sse-debug.ts +18 -0
- package/src/utils/stream-markup-healing.ts +247 -0
- package/src/utils/thinking-loop.ts +552 -0
- package/src/utils/tool-call-loop-guard.ts +107 -0
- package/src/utils/tool-choice.ts +101 -0
- package/src/utils/validation.ts +1932 -0
- package/src/utils.ts +492 -0
|
@@ -0,0 +1,3500 @@
|
|
|
1
|
+
import type { Effort } from "@linxiraos/pi-catalog/effort";
|
|
2
|
+
import { toFirepassWireModelId, toFireworksWireModelId } from "@linxiraos/pi-catalog/fireworks-model-id";
|
|
3
|
+
import { isGlm52ReasoningEffortModelId, isKimiK3ModelId } from "@linxiraos/pi-catalog/identity";
|
|
4
|
+
import { getSupportedEfforts } from "@linxiraos/pi-catalog/model-thinking";
|
|
5
|
+
import { calculateCost } from "@linxiraos/pi-catalog/models";
|
|
6
|
+
import type {
|
|
7
|
+
OpenAICompat,
|
|
8
|
+
OpenAIReasoningDisableMode,
|
|
9
|
+
OpenAIStreamMarkupHealingPattern,
|
|
10
|
+
OpenRouterRouting,
|
|
11
|
+
ResolvedOpenAICompat,
|
|
12
|
+
ResolvedOpenAIResponsesCompat,
|
|
13
|
+
ResolvedOpenAISharedCompat,
|
|
14
|
+
VercelGatewayRouting,
|
|
15
|
+
} from "@linxiraos/pi-catalog/types";
|
|
16
|
+
import { parseAlibabaTokenPlanCredential } from "@linxiraos/pi-catalog/wire/alibaba-token-plan";
|
|
17
|
+
import {
|
|
18
|
+
COREWEAVE_PROJECT_HEADER,
|
|
19
|
+
coreWeaveProjectHeaders,
|
|
20
|
+
hasCoreWeaveProjectHeader,
|
|
21
|
+
removeBlankCoreWeaveProjectHeaders,
|
|
22
|
+
} from "@linxiraos/pi-catalog/wire/coreweave";
|
|
23
|
+
import { parseGitHubCopilotApiKey } from "@linxiraos/pi-catalog/wire/github-copilot";
|
|
24
|
+
import {
|
|
25
|
+
$env,
|
|
26
|
+
classifyJsonPrefix,
|
|
27
|
+
extractHttpStatusFromError,
|
|
28
|
+
logger,
|
|
29
|
+
parseImageMetadata,
|
|
30
|
+
parseStreamingJson,
|
|
31
|
+
parseStreamingJsonThrottled,
|
|
32
|
+
stringifyJson,
|
|
33
|
+
structuredCloneJSON,
|
|
34
|
+
} from "@linxiraos/pi-utils";
|
|
35
|
+
import * as AIError from "../error";
|
|
36
|
+
import {
|
|
37
|
+
type Api,
|
|
38
|
+
type AssistantMessage,
|
|
39
|
+
type CacheRetention,
|
|
40
|
+
type ComputerAction,
|
|
41
|
+
type ComputerToolCallMetadata,
|
|
42
|
+
type Context,
|
|
43
|
+
type ImageContent,
|
|
44
|
+
type Message,
|
|
45
|
+
type MessageAttribution,
|
|
46
|
+
type Model,
|
|
47
|
+
OPENAI_MAX_OUTPUT_TOKENS,
|
|
48
|
+
type ServiceTier,
|
|
49
|
+
type StopReason,
|
|
50
|
+
type StreamOptions,
|
|
51
|
+
shouldSendServiceTier,
|
|
52
|
+
type TextContent,
|
|
53
|
+
type TextSignatureV1,
|
|
54
|
+
type ThinkingContent,
|
|
55
|
+
type Tool,
|
|
56
|
+
type ToolCall,
|
|
57
|
+
type ToolResultMessage,
|
|
58
|
+
type Usage,
|
|
59
|
+
} from "../types";
|
|
60
|
+
|
|
61
|
+
export type { OpenAIPromptCacheOptions } from "../types";
|
|
62
|
+
|
|
63
|
+
import {
|
|
64
|
+
getOpenAIResponsesHistoryItems,
|
|
65
|
+
getOpenAIResponsesHistoryPayload,
|
|
66
|
+
normalizeResponsesToolCallId,
|
|
67
|
+
normalizeSystemPrompts,
|
|
68
|
+
resolveCacheRetention,
|
|
69
|
+
sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
|
|
70
|
+
sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
|
|
71
|
+
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
72
|
+
stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay,
|
|
73
|
+
} from "../utils";
|
|
74
|
+
import {
|
|
75
|
+
clearStreamingPartialJson,
|
|
76
|
+
kStreamingArgumentsDone,
|
|
77
|
+
kStreamingLastParseLen,
|
|
78
|
+
kStreamingPartialJson,
|
|
79
|
+
} from "../utils/block-symbols";
|
|
80
|
+
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
81
|
+
import {
|
|
82
|
+
escapeHarmonyControlTokens,
|
|
83
|
+
escapeHarmonyControlTokensInJson,
|
|
84
|
+
isHarmonyDialectModel,
|
|
85
|
+
} from "../utils/harmony-leak";
|
|
86
|
+
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
87
|
+
import { getOpenRouterHeaders } from "../utils/openrouter-headers";
|
|
88
|
+
import { isForcedToolChoice } from "../utils/tool-choice";
|
|
89
|
+
import {
|
|
90
|
+
buildCopilotDynamicHeaders,
|
|
91
|
+
hasCopilotVisionInput,
|
|
92
|
+
resolveGitHubCopilotBaseUrl,
|
|
93
|
+
} from "./github-copilot-headers";
|
|
94
|
+
import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
|
|
95
|
+
import type { InputItem } from "./openai-codex/request-transformer";
|
|
96
|
+
import type {
|
|
97
|
+
Response as OpenAIResponse,
|
|
98
|
+
ResponseComputerToolCall,
|
|
99
|
+
ResponseContentPartAddedEvent,
|
|
100
|
+
ResponseCreateParamsStreaming,
|
|
101
|
+
ResponseCustomToolCall,
|
|
102
|
+
ResponseFunctionToolCall,
|
|
103
|
+
ResponseInput,
|
|
104
|
+
ResponseInputContent,
|
|
105
|
+
ResponseInputImage,
|
|
106
|
+
ResponseInputItem,
|
|
107
|
+
ResponseInputText,
|
|
108
|
+
ResponseOutputItem,
|
|
109
|
+
ResponseOutputMessage,
|
|
110
|
+
ResponseReasoningItem,
|
|
111
|
+
ResponseStatus,
|
|
112
|
+
ResponseStreamEvent,
|
|
113
|
+
} from "./openai-responses-wire";
|
|
114
|
+
import { transformMessages } from "./transform-messages";
|
|
115
|
+
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Keyless-provider sentinel. Custom providers configured with `auth: none`
|
|
119
|
+
* (models.yml) have no credential, so the coding-agent resolves their API key
|
|
120
|
+
* to this literal instead of a real secret. Providers must treat it as "no
|
|
121
|
+
* credential" and suppress any credential-bearing header (e.g. `Authorization:
|
|
122
|
+
* Bearer …`) rather than forwarding the sentinel on the wire. See #6188; the
|
|
123
|
+
* google-vertex and amazon-bedrock transports apply the same guard inline.
|
|
124
|
+
*/
|
|
125
|
+
export const NO_AUTH_SENTINEL = "N/A";
|
|
126
|
+
|
|
127
|
+
export interface OpenAIModelIdentity {
|
|
128
|
+
provider: string;
|
|
129
|
+
id: string;
|
|
130
|
+
baseUrl?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface OpenAIStrictToolsScope {
|
|
134
|
+
provider: string;
|
|
135
|
+
baseUrl: string | undefined;
|
|
136
|
+
modelId: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface OpenAIStrictToolsState {
|
|
140
|
+
strictTools: {
|
|
141
|
+
disabledModelScopes: Set<string>;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface OpenAIRequestSetupModel extends OpenAIModelIdentity {
|
|
146
|
+
headers?: Record<string, string>;
|
|
147
|
+
premiumMultiplier?: number;
|
|
148
|
+
compat?: Pick<ResolvedOpenAISharedCompat, "promptCacheSessionHeader">;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Cache identity controls shared by OpenAI-family transports. */
|
|
152
|
+
export interface OpenAICacheOptions {
|
|
153
|
+
cacheRetention?: CacheRetention;
|
|
154
|
+
sessionId?: string;
|
|
155
|
+
promptCacheKey?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface OpenAIRequestSetupOptions {
|
|
159
|
+
apiKey?: string;
|
|
160
|
+
extraHeaders?: Record<string, string>;
|
|
161
|
+
initiatorOverride?: MessageAttribution;
|
|
162
|
+
messages: Message[];
|
|
163
|
+
defaultBaseUrl?: string;
|
|
164
|
+
prependHeaders?: () => Record<string, string>;
|
|
165
|
+
alibabaCodingPlanAuth?: boolean;
|
|
166
|
+
azureChatCompletions?: {
|
|
167
|
+
apiVersion: string;
|
|
168
|
+
deploymentName: string;
|
|
169
|
+
};
|
|
170
|
+
openAISessionId?: string;
|
|
171
|
+
promptCacheSessionId?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface OpenAIRequestSetup {
|
|
175
|
+
copilotPremiumRequests: number | undefined;
|
|
176
|
+
baseUrl: string | undefined;
|
|
177
|
+
headers: Record<string, string>;
|
|
178
|
+
query: Record<string, string> | undefined;
|
|
179
|
+
requestHeaders: Record<string, string>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
183
|
+
const value = baseUrl?.trim();
|
|
184
|
+
if (!value) return undefined;
|
|
185
|
+
const normalized = value.replace(/\/+$/, "");
|
|
186
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveSakanaRequestBaseUrl(): string | undefined {
|
|
190
|
+
return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function applyCoreWeaveProjectHeader(headers: Record<string, string>): void {
|
|
194
|
+
removeBlankCoreWeaveProjectHeaders(headers);
|
|
195
|
+
if (hasCoreWeaveProjectHeader(headers)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const projectHeaders = coreWeaveProjectHeaders($env);
|
|
199
|
+
if (projectHeaders) {
|
|
200
|
+
headers[COREWEAVE_PROJECT_HEADER] = projectHeaders[COREWEAVE_PROJECT_HEADER];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function setHeaderIfAbsent(headers: Record<string, string>, name: string, value: string): void {
|
|
205
|
+
const normalizedName = name.toLowerCase();
|
|
206
|
+
for (const existingName in headers) {
|
|
207
|
+
if (existingName.toLowerCase() === normalizedName) return;
|
|
208
|
+
}
|
|
209
|
+
headers[name] = value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function resolveOpenAIRequestSetup(
|
|
213
|
+
model: OpenAIRequestSetupModel,
|
|
214
|
+
options: OpenAIRequestSetupOptions,
|
|
215
|
+
): OpenAIRequestSetup {
|
|
216
|
+
let apiKey = options.apiKey;
|
|
217
|
+
if (!apiKey) {
|
|
218
|
+
if (!$env.OPENAI_API_KEY) {
|
|
219
|
+
throw new AIError.MissingApiKeyError(
|
|
220
|
+
undefined,
|
|
221
|
+
"OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
apiKey = $env.OPENAI_API_KEY;
|
|
225
|
+
}
|
|
226
|
+
const rawApiKey = apiKey;
|
|
227
|
+
let headers = { ...(model.headers ?? {}) };
|
|
228
|
+
if (model.provider === "openrouter") {
|
|
229
|
+
Object.assign(headers, getOpenRouterHeaders());
|
|
230
|
+
}
|
|
231
|
+
Object.assign(headers, options.extraHeaders);
|
|
232
|
+
if (model.provider === "coreweave") {
|
|
233
|
+
applyCoreWeaveProjectHeader(headers);
|
|
234
|
+
}
|
|
235
|
+
if (options.prependHeaders) {
|
|
236
|
+
headers = { ...options.prependHeaders(), ...headers };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let copilotPremiumRequests: number | undefined;
|
|
240
|
+
let baseUrl = model.baseUrl;
|
|
241
|
+
if (model.provider === "moonshot") {
|
|
242
|
+
// Bundled `moonshot` catalog models hardcode the international endpoint
|
|
243
|
+
// (`api.moonshot.ai`). MOONSHOT_BASE_URL lets users redirect the provider
|
|
244
|
+
// at the China platform (`api.moonshot.cn`), which only accepts China keys
|
|
245
|
+
// and rejects the international host. (#2883)
|
|
246
|
+
const moonshotBaseUrl = $env.MOONSHOT_BASE_URL?.trim();
|
|
247
|
+
if (moonshotBaseUrl) {
|
|
248
|
+
baseUrl = moonshotBaseUrl;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (model.provider === "sakana") {
|
|
252
|
+
const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
|
|
253
|
+
if (sakanaBaseUrl) {
|
|
254
|
+
baseUrl = sakanaBaseUrl;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (model.provider === "github-copilot") {
|
|
258
|
+
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
259
|
+
const copilot = buildCopilotDynamicHeaders({
|
|
260
|
+
messages: options.messages,
|
|
261
|
+
hasImages: hasCopilotVisionInput(options.messages),
|
|
262
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
263
|
+
headers,
|
|
264
|
+
initiatorOverride: options.initiatorOverride,
|
|
265
|
+
});
|
|
266
|
+
Object.assign(headers, copilot.headers);
|
|
267
|
+
copilotPremiumRequests = copilot.premiumRequests;
|
|
268
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (model.provider === "alibaba-token-plan") {
|
|
272
|
+
// Require an explicitly resolved Token Plan credential. The generic
|
|
273
|
+
// `$env.OPENAI_API_KEY` fallback above matches the broad `sk-*` token
|
|
274
|
+
// grammar and would otherwise be sent to QwenCloud as bearer material.
|
|
275
|
+
if (!options.apiKey) {
|
|
276
|
+
throw new AIError.MissingApiKeyError("alibaba-token-plan");
|
|
277
|
+
}
|
|
278
|
+
const credential = parseAlibabaTokenPlanCredential(rawApiKey);
|
|
279
|
+
if (!credential) throw new AIError.ConfigurationError("Invalid QwenCloud Token Plan credential");
|
|
280
|
+
apiKey = credential.token;
|
|
281
|
+
if (credential.baseUrl) baseUrl = credential.baseUrl;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (options.alibabaCodingPlanAuth && model.provider === "alibaba-coding-plan") {
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(rawApiKey);
|
|
287
|
+
if (typeof parsed?.token === "string") {
|
|
288
|
+
apiKey = parsed.token;
|
|
289
|
+
}
|
|
290
|
+
if (typeof parsed?.enterpriseUrl === "string") {
|
|
291
|
+
baseUrl = parsed.enterpriseUrl;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Not JSON — use raw apiKey and catalog baseUrl.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let query: Record<string, string> | undefined;
|
|
299
|
+
if (options.azureChatCompletions && baseUrl?.includes(".openai.azure.com")) {
|
|
300
|
+
if (!baseUrl.includes("/deployments/")) {
|
|
301
|
+
baseUrl = `${baseUrl}/deployments/${options.azureChatCompletions.deploymentName}`;
|
|
302
|
+
}
|
|
303
|
+
query = { "api-version": options.azureChatCompletions.apiVersion };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (options.openAISessionId && model.provider === "openai") {
|
|
307
|
+
setHeaderIfAbsent(headers, "session_id", options.openAISessionId);
|
|
308
|
+
setHeaderIfAbsent(headers, "x-client-request-id", options.openAISessionId);
|
|
309
|
+
}
|
|
310
|
+
if (options.promptCacheSessionId && model.compat?.promptCacheSessionHeader) {
|
|
311
|
+
setHeaderIfAbsent(headers, model.compat.promptCacheSessionHeader, options.promptCacheSessionId);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (options.defaultBaseUrl !== undefined) {
|
|
315
|
+
baseUrl = baseUrl ?? ($env.OPENAI_BASE_URL?.trim() || options.defaultBaseUrl);
|
|
316
|
+
}
|
|
317
|
+
const requestHeaders = { ...headers };
|
|
318
|
+
// A keyless provider (`auth: none` in models.yml) resolves to the `N/A`
|
|
319
|
+
// sentinel rather than a real key. Injecting `Authorization: Bearer N/A`
|
|
320
|
+
// breaks custom endpoints that authenticate via their own headers (e.g.
|
|
321
|
+
// `headers.x-api-key`) and reject the bogus bearer — mirror the sentinel
|
|
322
|
+
// guards in google-vertex / amazon-bedrock and send no Authorization here
|
|
323
|
+
// (#6188). A caller-supplied Authorization in `model.headers` still wins.
|
|
324
|
+
if (apiKey !== NO_AUTH_SENTINEL) {
|
|
325
|
+
headers.Authorization ??= `Bearer ${apiKey}`;
|
|
326
|
+
}
|
|
327
|
+
return { copilotPremiumRequests, baseUrl, headers, query, requestHeaders };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function applyOpenAIServiceTier(
|
|
331
|
+
params: { service_tier?: ServiceTier | null | undefined },
|
|
332
|
+
serviceTier: ServiceTier | null | undefined,
|
|
333
|
+
model: Pick<Model, "provider" | "api" | "id">,
|
|
334
|
+
): void {
|
|
335
|
+
if (!shouldSendServiceTier(serviceTier, model)) return;
|
|
336
|
+
params.service_tier = serviceTier;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Standard OpenAI Responses service-tier cost multipliers. The non-Codex
|
|
341
|
+
* Responses path bills the tier it was served (or requested): Flex processing is
|
|
342
|
+
* half price; Priority is a 2x premium. Codex bills the same tiers with its own
|
|
343
|
+
* table (Priority is 2.5x on gpt-5.5) and applies that separately.
|
|
344
|
+
*/
|
|
345
|
+
function getOpenAIResponsesServiceTierCostMultiplier(tier: string | null | undefined): number {
|
|
346
|
+
switch (tier) {
|
|
347
|
+
case "flex":
|
|
348
|
+
return 0.5;
|
|
349
|
+
case "priority":
|
|
350
|
+
return 2;
|
|
351
|
+
default:
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Adjust resolved cost by the service tier OpenAI actually billed — parity with
|
|
358
|
+
* Codex (`applyCodexServiceTierPricing`), but with the standard (non-Codex)
|
|
359
|
+
* multipliers. The served tier comes from the response echo, falling back to the
|
|
360
|
+
* resolved request tier. Scoped to `provider: "openai"` (the only standard
|
|
361
|
+
* Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
|
|
362
|
+
* proxy can never skew those costs.
|
|
363
|
+
*/
|
|
364
|
+
export function applyOpenAIResponsesServiceTierCost(
|
|
365
|
+
model: Pick<Model, "provider">,
|
|
366
|
+
usage: AssistantMessage["usage"],
|
|
367
|
+
responseServiceTier: unknown,
|
|
368
|
+
requestServiceTier: ServiceTier | null | undefined,
|
|
369
|
+
): void {
|
|
370
|
+
if (model.provider !== "openai") return;
|
|
371
|
+
// The response echo is authoritative when present (OpenAI may downgrade a
|
|
372
|
+
// requested priority/flex turn to default under load); only fall back to the
|
|
373
|
+
// requested tier when the response omits the echo entirely.
|
|
374
|
+
const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
|
|
375
|
+
const multiplier = getOpenAIResponsesServiceTierCostMultiplier(served);
|
|
376
|
+
if (multiplier === 1) return;
|
|
377
|
+
usage.cost.input *= multiplier;
|
|
378
|
+
usage.cost.output *= multiplier;
|
|
379
|
+
usage.cost.cacheRead *= multiplier;
|
|
380
|
+
usage.cost.cacheWrite *= multiplier;
|
|
381
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Reconcile token-price estimates with OpenRouter's authoritative account charge. */
|
|
385
|
+
export function applyOpenRouterReportedCost(model: Pick<Model, "provider">, usage: Usage, rawUsage: unknown): void {
|
|
386
|
+
if (model.provider !== "openrouter" || typeof rawUsage !== "object" || rawUsage === null) return;
|
|
387
|
+
const reportedCost = Reflect.get(rawUsage, "cost");
|
|
388
|
+
if (typeof reportedCost !== "number" || !Number.isFinite(reportedCost) || reportedCost < 0) return;
|
|
389
|
+
|
|
390
|
+
const estimatedCost = usage.cost.total;
|
|
391
|
+
if (Number.isFinite(estimatedCost) && estimatedCost > 0) {
|
|
392
|
+
const scale = reportedCost / estimatedCost;
|
|
393
|
+
usage.cost.input *= scale;
|
|
394
|
+
usage.cost.output *= scale;
|
|
395
|
+
usage.cost.cacheRead *= scale;
|
|
396
|
+
usage.cost.cacheWrite *= scale;
|
|
397
|
+
} else {
|
|
398
|
+
// Keep legacy component-only aggregators additive when catalog pricing is unavailable.
|
|
399
|
+
usage.cost.input = reportedCost;
|
|
400
|
+
usage.cost.output = 0;
|
|
401
|
+
usage.cost.cacheRead = 0;
|
|
402
|
+
usage.cost.cacheWrite = 0;
|
|
403
|
+
}
|
|
404
|
+
usage.cost.total = reportedCost;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface OpenAIUsageAccountingInput {
|
|
408
|
+
promptTokens: number;
|
|
409
|
+
outputTokens: number;
|
|
410
|
+
cachedTokens: number;
|
|
411
|
+
reasoningTokens: number;
|
|
412
|
+
cacheWriteOpenRouter: number | undefined;
|
|
413
|
+
cacheWriteDeepSeek: number | undefined;
|
|
414
|
+
hasDeepSeekCacheHitAndMiss: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface OpenAIUsageAccounting {
|
|
418
|
+
input: number;
|
|
419
|
+
output: number;
|
|
420
|
+
cacheRead: number;
|
|
421
|
+
cacheWrite: number;
|
|
422
|
+
totalTokens: number;
|
|
423
|
+
reasoningTokens?: number;
|
|
424
|
+
orchestration?: Usage["orchestration"];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting {
|
|
428
|
+
const cacheWriteTokens = accounting.cacheWriteOpenRouter ?? accounting.cacheWriteDeepSeek ?? 0;
|
|
429
|
+
const isDeepSeekUsage =
|
|
430
|
+
accounting.hasDeepSeekCacheHitAndMiss &&
|
|
431
|
+
accounting.cacheWriteOpenRouter === undefined &&
|
|
432
|
+
(accounting.cacheWriteDeepSeek ?? 0) > 0;
|
|
433
|
+
const input = isDeepSeekUsage
|
|
434
|
+
? Math.max(0, accounting.promptTokens - accounting.cachedTokens)
|
|
435
|
+
: Math.max(0, accounting.promptTokens - accounting.cachedTokens - cacheWriteTokens);
|
|
436
|
+
const cacheWrite = isDeepSeekUsage ? 0 : cacheWriteTokens;
|
|
437
|
+
return {
|
|
438
|
+
input,
|
|
439
|
+
output: accounting.outputTokens,
|
|
440
|
+
cacheRead: accounting.cachedTokens,
|
|
441
|
+
cacheWrite,
|
|
442
|
+
totalTokens: input + accounting.outputTokens + accounting.cachedTokens + cacheWrite,
|
|
443
|
+
...(accounting.reasoningTokens > 0 ? { reasoningTokens: accounting.reasoningTokens } : {}),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Normalize a cache identity to the wire limit accepted by OpenAI-family providers. */
|
|
448
|
+
export function normalizeOpenAIPromptCacheKey(sessionId: string | undefined): string | undefined {
|
|
449
|
+
return normalizeOpenAIStableId(sessionId, 64, "pc_");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function normalizeOpenRouterResponsesSessionId(sessionId: string | undefined): string | undefined {
|
|
453
|
+
return normalizeOpenAIStableId(sessionId, 256, "session_");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Resolve a prompt-cache identity, falling back to the provider session unless caching is disabled. */
|
|
457
|
+
export function getOpenAIPromptCacheKey(options: OpenAICacheOptions | undefined): string | undefined {
|
|
458
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
459
|
+
return normalizeOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function getOpenAIResponsesRoutingSessionId(
|
|
463
|
+
options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined,
|
|
464
|
+
): string | undefined {
|
|
465
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
466
|
+
return normalizeOpenAIPromptCacheKey(options?.sessionId);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function getOpenRouterResponsesSessionId(
|
|
470
|
+
options: Pick<OpenAICacheOptions, "cacheRetention" | "sessionId"> | undefined,
|
|
471
|
+
): string | undefined {
|
|
472
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
473
|
+
return normalizeOpenRouterResponsesSessionId(options?.sessionId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string> {
|
|
477
|
+
const map = new Map<string, string>();
|
|
478
|
+
if (!value) return map;
|
|
479
|
+
for (const entry of value.split(",")) {
|
|
480
|
+
const trimmed = entry.trim();
|
|
481
|
+
if (!trimmed) continue;
|
|
482
|
+
const [modelId, deploymentName] = trimmed.split("=", 2);
|
|
483
|
+
if (!modelId || !deploymentName) continue;
|
|
484
|
+
map.set(modelId.trim(), deploymentName.trim());
|
|
485
|
+
}
|
|
486
|
+
return map;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function createOpenAIStrictToolsState(): OpenAIStrictToolsState {
|
|
490
|
+
return {
|
|
491
|
+
strictTools: {
|
|
492
|
+
disabledModelScopes: new Set<string>(),
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function clearOpenAIStrictToolsState(state: OpenAIStrictToolsState): void {
|
|
498
|
+
state.strictTools.disabledModelScopes.clear();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function getOpenAIStrictToolsScope(
|
|
502
|
+
model: OpenAIModelIdentity,
|
|
503
|
+
resolvedBaseUrl: string | undefined,
|
|
504
|
+
): OpenAIStrictToolsScope {
|
|
505
|
+
return {
|
|
506
|
+
provider: model.provider,
|
|
507
|
+
baseUrl: resolvedBaseUrl ?? model.baseUrl,
|
|
508
|
+
modelId: model.id,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function isStrictToolsDisabledForScope(
|
|
513
|
+
state: OpenAIStrictToolsState | undefined,
|
|
514
|
+
scope: OpenAIStrictToolsScope | undefined,
|
|
515
|
+
): boolean {
|
|
516
|
+
if (!scope) return false;
|
|
517
|
+
return (
|
|
518
|
+
state?.strictTools.disabledModelScopes.has(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`) ?? false
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function disableStrictToolsForScope(
|
|
523
|
+
state: OpenAIStrictToolsState | undefined,
|
|
524
|
+
scope: OpenAIStrictToolsScope | undefined,
|
|
525
|
+
): void {
|
|
526
|
+
if (!scope) return;
|
|
527
|
+
state?.strictTools.disabledModelScopes.add(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function isOpenRouterAnthropicModel(model: OpenAIModelIdentity): boolean {
|
|
531
|
+
return model.provider === "openrouter" && model.id.toLowerCase().startsWith("anthropic/");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Append an OpenRouter routing-variant suffix (e.g. `:nitro`, `:floor`, `:online`, `:exacto`)
|
|
536
|
+
* to a model id when no explicit variant is already present. A variant is considered
|
|
537
|
+
* "already present" when `modelId` contains a colon after the last `/` separator —
|
|
538
|
+
* which covers both user-typed selectors (`anthropic/claude-haiku:nitro`) and catalog
|
|
539
|
+
* entries that bake the variant in (`deepseek/deepseek-v3.1-terminus:exacto`).
|
|
540
|
+
*/
|
|
541
|
+
export function applyOpenRouterRoutingVariant(modelId: string, variant: string | undefined): string {
|
|
542
|
+
if (!variant) return modelId;
|
|
543
|
+
const lastSlash = modelId.lastIndexOf("/");
|
|
544
|
+
const lastColon = modelId.lastIndexOf(":");
|
|
545
|
+
if (lastColon > lastSlash) return modelId;
|
|
546
|
+
return `${modelId}:${variant}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function applyWireModelIdTransform(
|
|
550
|
+
baseId: string,
|
|
551
|
+
mode: ResolvedOpenAISharedCompat["wireModelIdMode"],
|
|
552
|
+
openrouterVariant?: string,
|
|
553
|
+
): string {
|
|
554
|
+
switch (mode) {
|
|
555
|
+
case "firepass":
|
|
556
|
+
return toFirepassWireModelId(baseId);
|
|
557
|
+
case "fireworks":
|
|
558
|
+
return toFireworksWireModelId(baseId);
|
|
559
|
+
case "openrouter":
|
|
560
|
+
return applyOpenRouterRoutingVariant(baseId, openrouterVariant);
|
|
561
|
+
default:
|
|
562
|
+
return baseId;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export interface OpenAIOutputTokenParam {
|
|
567
|
+
field: "max_tokens" | "max_completion_tokens" | "max_output_tokens";
|
|
568
|
+
value: number;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export interface ResolveOpenAIOutputTokenInput {
|
|
572
|
+
/** Wire field the endpoint expects for the output cap. */
|
|
573
|
+
field: OpenAIOutputTokenParam["field"];
|
|
574
|
+
/** Caller-supplied output cap (model-defaulted by `stream.ts`, or null/undefined on direct provider calls). */
|
|
575
|
+
maxTokens: number | null | undefined;
|
|
576
|
+
/** Whether the caller explicitly set `maxTokens` (routing omission only applies when false). */
|
|
577
|
+
maxTokensExplicit: boolean;
|
|
578
|
+
/** Model output cap (`model.maxTokens`). */
|
|
579
|
+
modelMaxTokens: number | null | undefined;
|
|
580
|
+
/** Drop the field entirely — proxies with unknown upstream caps (Ollama via `model.omitMaxOutputTokens`). */
|
|
581
|
+
omitMaxOutputTokens: boolean;
|
|
582
|
+
/** The model sits behind OpenRouter (catalog default caps are omitted so each upstream self-caps). */
|
|
583
|
+
isOpenRouterHost: boolean;
|
|
584
|
+
/** Endpoint always needs a cap (Kimi-family TPM math); supplies the model default when the caller did not. */
|
|
585
|
+
alwaysSendMaxTokens: boolean;
|
|
586
|
+
/** Hard provider clamp; defaults to {@link OPENAI_MAX_OUTPUT_TOKENS}. */
|
|
587
|
+
providerOutputClamp?: number;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Resolve the single output-token wire parameter shared by Chat Completions
|
|
592
|
+
* (`max_tokens`/`max_completion_tokens`) and the Responses family
|
|
593
|
+
* (`max_output_tokens`). Centralizes the provider exceptions that previously
|
|
594
|
+
* lived inline in both `buildParams`:
|
|
595
|
+
* - `alwaysSendMaxTokens`: Kimi-family endpoints derive TPM limits from the
|
|
596
|
+
* cap and require one on every call, so default from the model cap (or
|
|
597
|
+
* {@link OPENAI_MAX_OUTPUT_TOKENS}) when the caller omitted it.
|
|
598
|
+
* - OpenRouter routing omission: OpenRouter fans out to upstreams whose output
|
|
599
|
+
* caps differ from the catalog value, so a catalog default above the routed
|
|
600
|
+
* upstream's cap makes OpenRouter skip that upstream. Omit catalog defaults
|
|
601
|
+
* (explicit caller caps still win) so `provider.order`/`only` is honored.
|
|
602
|
+
* - model/provider clamp: never exceed `model.maxTokens` or the provider clamp
|
|
603
|
+
* (`OPENAI_MAX_OUTPUT_TOKENS`, raised for GLM-5.2 reasoning by the caller).
|
|
604
|
+
* - `omitMaxOutputTokens`: proxies (Ollama) with unknown upstream caps drop it.
|
|
605
|
+
*/
|
|
606
|
+
export function resolveOpenAIOutputTokenParam(
|
|
607
|
+
input: ResolveOpenAIOutputTokenInput,
|
|
608
|
+
): OpenAIOutputTokenParam | undefined {
|
|
609
|
+
if (input.omitMaxOutputTokens) return undefined;
|
|
610
|
+
const requested =
|
|
611
|
+
input.maxTokens ?? (input.alwaysSendMaxTokens ? (input.modelMaxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined);
|
|
612
|
+
if (requested === undefined) return undefined;
|
|
613
|
+
if (input.isOpenRouterHost && !input.alwaysSendMaxTokens && !input.maxTokensExplicit) return undefined;
|
|
614
|
+
const value = Math.min(
|
|
615
|
+
requested,
|
|
616
|
+
input.modelMaxTokens ?? Number.POSITIVE_INFINITY,
|
|
617
|
+
input.providerOutputClamp ?? OPENAI_MAX_OUTPUT_TOKENS,
|
|
618
|
+
);
|
|
619
|
+
if (!(value > 0)) return undefined;
|
|
620
|
+
return { field: input.field, value };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export interface OpenAIGatewayRoutingParams {
|
|
624
|
+
provider?: OpenRouterRouting;
|
|
625
|
+
providerOptions?: { gateway?: Pick<VercelGatewayRouting, "only" | "order" | "caching"> };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export interface OpenAIGatewayRoutingCompat {
|
|
629
|
+
isOpenRouterHost: boolean;
|
|
630
|
+
openRouterRouting?: OpenRouterRouting;
|
|
631
|
+
isVercelGatewayHost?: boolean;
|
|
632
|
+
vercelGatewayRouting?: VercelGatewayRouting;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Apply gateway routing preferences to the request body. OpenRouter routes via
|
|
637
|
+
* the top-level `provider` field; the Vercel AI Gateway routes Chat
|
|
638
|
+
* Completions through `providerOptions.gateway`.
|
|
639
|
+
*/
|
|
640
|
+
export function applyOpenAIGatewayRouting(
|
|
641
|
+
params: OpenAIGatewayRoutingParams,
|
|
642
|
+
compat: OpenAIGatewayRoutingCompat,
|
|
643
|
+
cacheEnabled = true,
|
|
644
|
+
): void {
|
|
645
|
+
if (compat.isOpenRouterHost && compat.openRouterRouting) {
|
|
646
|
+
params.provider = compat.openRouterRouting;
|
|
647
|
+
}
|
|
648
|
+
if (compat.isVercelGatewayHost && compat.vercelGatewayRouting) {
|
|
649
|
+
const routing = compat.vercelGatewayRouting;
|
|
650
|
+
if (routing.only || routing.order || (cacheEnabled && routing.caching)) {
|
|
651
|
+
const gatewayOptions: Pick<VercelGatewayRouting, "only" | "order" | "caching"> = {};
|
|
652
|
+
if (routing.only) gatewayOptions.only = routing.only;
|
|
653
|
+
if (routing.order) gatewayOptions.order = routing.order;
|
|
654
|
+
if (cacheEnabled && routing.caching) gatewayOptions.caching = routing.caching;
|
|
655
|
+
params.providerOptions = { gateway: gatewayOptions };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export interface VercelResponsesCacheParams {
|
|
661
|
+
caching?: "auto";
|
|
662
|
+
cache_anchor_items?: number;
|
|
663
|
+
cache_ttl?: "5m" | "1h";
|
|
664
|
+
providerOptions?: { gateway?: Pick<VercelGatewayRouting, "only" | "order"> };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export interface VercelResponsesCacheCompat {
|
|
668
|
+
isVercelGatewayHost: boolean;
|
|
669
|
+
vercelGatewayRouting?: VercelGatewayRouting;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Apply Vercel AI Gateway's Responses-only automatic cache controls and
|
|
674
|
+
* provider routing. Cache settings are top-level Responses fields, while
|
|
675
|
+
* `only` and `order` remain under `providerOptions.gateway`.
|
|
676
|
+
*/
|
|
677
|
+
export function applyVercelResponsesCacheControls(
|
|
678
|
+
params: VercelResponsesCacheParams,
|
|
679
|
+
compat: VercelResponsesCacheCompat,
|
|
680
|
+
cacheRetention: CacheRetention = "short",
|
|
681
|
+
): void {
|
|
682
|
+
const routing = compat.vercelGatewayRouting;
|
|
683
|
+
if (!compat.isVercelGatewayHost) return;
|
|
684
|
+
|
|
685
|
+
if (routing?.only || routing?.order) {
|
|
686
|
+
const gateway: Pick<VercelGatewayRouting, "only" | "order"> = {};
|
|
687
|
+
if (routing.only) gateway.only = routing.only;
|
|
688
|
+
if (routing.order) gateway.order = routing.order;
|
|
689
|
+
params.providerOptions = { gateway };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (cacheRetention === "none" || routing?.caching !== "auto") return;
|
|
693
|
+
|
|
694
|
+
params.caching = "auto";
|
|
695
|
+
if (routing.cacheAnchorItems !== undefined) params.cache_anchor_items = routing.cacheAnchorItems;
|
|
696
|
+
// A configured 1h TTL is capped by resolved retention; default and short intentionally omit it.
|
|
697
|
+
if (routing.cacheTtl !== undefined && (routing.cacheTtl !== "1h" || cacheRetention === "long")) {
|
|
698
|
+
params.cache_ttl = routing.cacheTtl;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export interface OpenAIExtraBodyOptions {
|
|
703
|
+
/**
|
|
704
|
+
* Fireworks rejects DeepSeek-style `thinking` toggles alongside OpenAI-style
|
|
705
|
+
* `reasoning_effort`; drop `thinking` when the effort field carries the level.
|
|
706
|
+
*/
|
|
707
|
+
dropThinkingWhenReasoningEffort?: boolean;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Merge a compat/options `extraBody` blob into the request params. When
|
|
712
|
+
* `dropThinkingWhenReasoningEffort` is set and `reasoning_effort` is present,
|
|
713
|
+
* delete the conflicting `thinking` toggle (Fireworks rejects both together).
|
|
714
|
+
*/
|
|
715
|
+
export function applyOpenAIExtraBody<P extends object>(
|
|
716
|
+
params: P,
|
|
717
|
+
extraBody: Record<string, unknown> | undefined,
|
|
718
|
+
options?: OpenAIExtraBodyOptions,
|
|
719
|
+
): void {
|
|
720
|
+
if (!extraBody) return;
|
|
721
|
+
Object.assign(params, extraBody);
|
|
722
|
+
if (options?.dropThinkingWhenReasoningEffort) {
|
|
723
|
+
const shaped = params as { reasoning_effort?: unknown; thinking?: unknown };
|
|
724
|
+
if (shaped.reasoning_effort !== undefined) {
|
|
725
|
+
delete shaped.thinking;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Chat Completions streaming request body shaped by the OpenAI-family providers.
|
|
732
|
+
* Extends the vendored SDK params with the compat dialect fields pi-ai emits
|
|
733
|
+
* (binary `thinking`, Qwen `enable_thinking`/`chat_template_kwargs`, nested
|
|
734
|
+
* `reasoning`, gateway `provider`/`providerOptions`, sampling extras). Lives in
|
|
735
|
+
* the shared module beside the request-shaping helpers that mutate it.
|
|
736
|
+
*/
|
|
737
|
+
export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming, "reasoning_effort" | "service_tier"> & {
|
|
738
|
+
top_k?: number;
|
|
739
|
+
min_p?: number;
|
|
740
|
+
repetition_penalty?: number;
|
|
741
|
+
thinking?: { type: "enabled" | "disabled"; effort?: string; keep?: "all" };
|
|
742
|
+
enable_thinking?: boolean;
|
|
743
|
+
preserve_thinking?: boolean;
|
|
744
|
+
chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
|
|
745
|
+
reasoning?: { effort?: string } | { enabled: false };
|
|
746
|
+
reasoning_effort?: string | null;
|
|
747
|
+
service_tier?: ServiceTier;
|
|
748
|
+
tool_stream?: boolean;
|
|
749
|
+
provider?: OpenAICompat["openRouterRouting"];
|
|
750
|
+
providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
/** Reasoning-relevant slice of caller options the Chat Completions dialect dispatch reads. */
|
|
754
|
+
export interface ChatCompletionsReasoningOptions {
|
|
755
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
756
|
+
disableReasoning?: boolean;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export type OpenAICompatEndpoint = "chat-completions" | "responses";
|
|
760
|
+
|
|
761
|
+
export type OpenAIReasoningDisableReason = "caller" | "forced-tool-choice" | "tool-choice" | "not-requested";
|
|
762
|
+
|
|
763
|
+
export type OpenAICompatPolicyCompat = ResolvedOpenAISharedCompat &
|
|
764
|
+
Partial<ResolvedOpenAICompat> &
|
|
765
|
+
Partial<ResolvedOpenAIResponsesCompat>;
|
|
766
|
+
|
|
767
|
+
export interface ResolveOpenAICompatPolicyOptions {
|
|
768
|
+
endpoint: OpenAICompatEndpoint;
|
|
769
|
+
compat?: OpenAICompatPolicyCompat;
|
|
770
|
+
reasoning?: string;
|
|
771
|
+
disableReasoning?: boolean;
|
|
772
|
+
toolChoice?: unknown;
|
|
773
|
+
strictResponsesPairing?: boolean;
|
|
774
|
+
includeEncryptedReasoning?: boolean;
|
|
775
|
+
filterReasoningHistory?: boolean;
|
|
776
|
+
omitReasoningEffort?: boolean;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export interface OpenAICompatPolicy {
|
|
780
|
+
endpoint: OpenAICompatEndpoint;
|
|
781
|
+
compat: OpenAICompatPolicyCompat;
|
|
782
|
+
reasoning: {
|
|
783
|
+
modelSupported: boolean;
|
|
784
|
+
supportsParams: boolean;
|
|
785
|
+
requestedEffort?: string;
|
|
786
|
+
wireEffort?: string;
|
|
787
|
+
enabled: boolean;
|
|
788
|
+
disabled: boolean;
|
|
789
|
+
disableReason?: OpenAIReasoningDisableReason;
|
|
790
|
+
dialect: ResolvedOpenAISharedCompat["thinkingFormat"];
|
|
791
|
+
disableMode: OpenAIReasoningDisableMode;
|
|
792
|
+
omitReasoningEffort: boolean;
|
|
793
|
+
includeEncryptedReasoning: boolean;
|
|
794
|
+
filterReasoningHistory: boolean;
|
|
795
|
+
requiresReasoningContentForToolCalls: boolean;
|
|
796
|
+
requiresReasoningContentForAllAssistantTurns: boolean;
|
|
797
|
+
allowsSyntheticReasoningContentForToolCalls: boolean;
|
|
798
|
+
reasoningContentField?: OpenAICompat["reasoningContentField"];
|
|
799
|
+
requiresThinkingAsText: boolean;
|
|
800
|
+
};
|
|
801
|
+
tools: {
|
|
802
|
+
strictResponsesPairing: boolean;
|
|
803
|
+
toolCallIdKind: "default" | "openai-40" | "mistral-9-alnum";
|
|
804
|
+
};
|
|
805
|
+
messages: {
|
|
806
|
+
systemRole: "system" | "developer";
|
|
807
|
+
supportsDeveloperRole: boolean;
|
|
808
|
+
supportsMultipleSystemMessages: boolean;
|
|
809
|
+
};
|
|
810
|
+
stream: {
|
|
811
|
+
stripSpecialTokens: "deepseek" | false;
|
|
812
|
+
markupHealingPattern?: OpenAIStreamMarkupHealingPattern;
|
|
813
|
+
reasoningDeltasMayBeCumulative: boolean;
|
|
814
|
+
emptyLengthFinishIsContextError: boolean;
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Map a user-facing effort to the provider wire value: explicit compat
|
|
820
|
+
* override first, then the model's baked `thinking.effortMap`, else identity.
|
|
821
|
+
* Shared by the chat-completions/Responses policy resolver and the Codex
|
|
822
|
+
* request transformer.
|
|
823
|
+
*/
|
|
824
|
+
export function mapOpenAIReasoningEffort(
|
|
825
|
+
model: Pick<Model, "thinking">,
|
|
826
|
+
compat: { reasoningEffortMap?: Partial<Record<Effort, string>> } | undefined,
|
|
827
|
+
effort: string,
|
|
828
|
+
): string {
|
|
829
|
+
const level = effort as Effort;
|
|
830
|
+
return compat?.reasoningEffortMap?.[level] ?? model.thinking?.effortMap?.[level] ?? effort;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function isImplicitDisableWhenNotRequested(disableMode: OpenAIReasoningDisableMode): boolean {
|
|
834
|
+
return (
|
|
835
|
+
disableMode === "zai-thinking-disabled" ||
|
|
836
|
+
disableMode === "qwen-enable-thinking-false" ||
|
|
837
|
+
disableMode === "qwen-template-false"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function resolveOpenAICompatPolicy<TApi extends Api>(
|
|
842
|
+
model: Model<TApi>,
|
|
843
|
+
options: ResolveOpenAICompatPolicyOptions,
|
|
844
|
+
): OpenAICompatPolicy {
|
|
845
|
+
const baseCompat = (options.compat ?? model.compat) as OpenAICompatPolicyCompat;
|
|
846
|
+
const requestedEffort = options.reasoning;
|
|
847
|
+
const modelSupported = Boolean(model.reasoning);
|
|
848
|
+
const forcedToolChoiceSuppressesReasoning =
|
|
849
|
+
baseCompat.disableReasoningOnForcedToolChoice &&
|
|
850
|
+
baseCompat.supportsForcedToolChoice &&
|
|
851
|
+
isForcedToolChoice(options.toolChoice);
|
|
852
|
+
const anyToolChoiceSuppressesReasoning =
|
|
853
|
+
!forcedToolChoiceSuppressesReasoning &&
|
|
854
|
+
baseCompat.disableReasoningOnToolChoice &&
|
|
855
|
+
options.toolChoice !== undefined;
|
|
856
|
+
const requestedAndAllowed = requestedEffort !== undefined && !options.disableReasoning && modelSupported;
|
|
857
|
+
const conflictDisableReason: OpenAIReasoningDisableReason | undefined = forcedToolChoiceSuppressesReasoning
|
|
858
|
+
? "forced-tool-choice"
|
|
859
|
+
: anyToolChoiceSuppressesReasoning
|
|
860
|
+
? "tool-choice"
|
|
861
|
+
: undefined;
|
|
862
|
+
const disableReason: OpenAIReasoningDisableReason | undefined = options.disableReasoning
|
|
863
|
+
? "caller"
|
|
864
|
+
: conflictDisableReason;
|
|
865
|
+
const enabledBeforeThinkingVariant = requestedAndAllowed && disableReason === undefined;
|
|
866
|
+
const baseWireEffort =
|
|
867
|
+
enabledBeforeThinkingVariant && requestedEffort !== undefined
|
|
868
|
+
? mapOpenAIReasoningEffort(model, baseCompat, requestedEffort)
|
|
869
|
+
: undefined;
|
|
870
|
+
const disabledByNoneEffort =
|
|
871
|
+
enabledBeforeThinkingVariant &&
|
|
872
|
+
baseCompat.reasoningDisableMode === "zai-thinking-disabled" &&
|
|
873
|
+
baseWireEffort === "none";
|
|
874
|
+
const enabled = enabledBeforeThinkingVariant && !disabledByNoneEffort;
|
|
875
|
+
const compat =
|
|
876
|
+
enabled && baseCompat.whenThinking ? (baseCompat.whenThinking as OpenAICompatPolicyCompat) : baseCompat;
|
|
877
|
+
const omitReasoningEffort =
|
|
878
|
+
options.omitReasoningEffort ?? (compat.omitReasoningEffort || !compat.supportsReasoningEffort);
|
|
879
|
+
const disableMode = compat.reasoningDisableMode;
|
|
880
|
+
let wireEffort =
|
|
881
|
+
enabled && requestedEffort !== undefined ? mapOpenAIReasoningEffort(model, compat, requestedEffort) : undefined;
|
|
882
|
+
const disabledWithoutRequest =
|
|
883
|
+
modelSupported &&
|
|
884
|
+
requestedEffort === undefined &&
|
|
885
|
+
!options.disableReasoning &&
|
|
886
|
+
isImplicitDisableWhenNotRequested(disableMode);
|
|
887
|
+
const disabled =
|
|
888
|
+
(modelSupported && disableReason === "caller") ||
|
|
889
|
+
conflictDisableReason !== undefined ||
|
|
890
|
+
(modelSupported && disabledWithoutRequest) ||
|
|
891
|
+
disabledByNoneEffort;
|
|
892
|
+
if (
|
|
893
|
+
disabled &&
|
|
894
|
+
disableReason === "caller" &&
|
|
895
|
+
requestedEffort === undefined &&
|
|
896
|
+
disableMode === "lowest-effort" &&
|
|
897
|
+
compat.supportsReasoningEffort &&
|
|
898
|
+
!omitReasoningEffort
|
|
899
|
+
) {
|
|
900
|
+
const minEffort = getSupportedEfforts(model)[0];
|
|
901
|
+
if (minEffort === undefined) {
|
|
902
|
+
throw new AIError.ConfigurationError(`Model ${model.provider}/${model.id} has no supported reasoning efforts`);
|
|
903
|
+
}
|
|
904
|
+
wireEffort = mapOpenAIReasoningEffort(model, compat, minEffort);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
endpoint: options.endpoint,
|
|
909
|
+
compat,
|
|
910
|
+
reasoning: {
|
|
911
|
+
modelSupported,
|
|
912
|
+
supportsParams: compat.supportsReasoningParams,
|
|
913
|
+
requestedEffort,
|
|
914
|
+
wireEffort,
|
|
915
|
+
enabled,
|
|
916
|
+
disabled,
|
|
917
|
+
disableReason: disableReason ?? (disabledWithoutRequest || disabledByNoneEffort ? "not-requested" : undefined),
|
|
918
|
+
dialect: compat.thinkingFormat,
|
|
919
|
+
requiresReasoningContentForToolCalls: compat.requiresReasoningContentForToolCalls,
|
|
920
|
+
requiresReasoningContentForAllAssistantTurns: compat.requiresReasoningContentForAllAssistantTurns,
|
|
921
|
+
allowsSyntheticReasoningContentForToolCalls: compat.allowsSyntheticReasoningContentForToolCalls,
|
|
922
|
+
reasoningContentField: compat.reasoningContentField,
|
|
923
|
+
requiresThinkingAsText: compat.requiresThinkingAsText,
|
|
924
|
+
disableMode,
|
|
925
|
+
omitReasoningEffort,
|
|
926
|
+
includeEncryptedReasoning: options.includeEncryptedReasoning ?? compat.includeEncryptedReasoning,
|
|
927
|
+
filterReasoningHistory: options.filterReasoningHistory ?? compat.filterReasoningHistory,
|
|
928
|
+
},
|
|
929
|
+
tools: {
|
|
930
|
+
strictResponsesPairing: options.strictResponsesPairing ?? compat.strictResponsesPairing ?? false,
|
|
931
|
+
toolCallIdKind: compat.requiresMistralToolIds
|
|
932
|
+
? "mistral-9-alnum"
|
|
933
|
+
: compat.usesOpenAIToolCallIdLimit
|
|
934
|
+
? "openai-40"
|
|
935
|
+
: "default",
|
|
936
|
+
},
|
|
937
|
+
messages: {
|
|
938
|
+
systemRole: modelSupported && compat.supportsDeveloperRole ? "developer" : "system",
|
|
939
|
+
supportsDeveloperRole: compat.supportsDeveloperRole,
|
|
940
|
+
supportsMultipleSystemMessages: compat.supportsMultipleSystemMessages ?? true,
|
|
941
|
+
},
|
|
942
|
+
stream: {
|
|
943
|
+
stripSpecialTokens: compat.stripDeepseekSpecialTokens ? "deepseek" : false,
|
|
944
|
+
markupHealingPattern: compat.streamMarkupHealingPattern,
|
|
945
|
+
reasoningDeltasMayBeCumulative: compat.reasoningDeltasMayBeCumulative,
|
|
946
|
+
emptyLengthFinishIsContextError: compat.emptyLengthFinishIsContextError,
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function encodeChatCompletionsDisabledReasoning(
|
|
952
|
+
params: OpenAICompletionsParams,
|
|
953
|
+
disableMode: OpenAIReasoningDisableMode,
|
|
954
|
+
): void {
|
|
955
|
+
delete params.reasoning_effort;
|
|
956
|
+
switch (disableMode) {
|
|
957
|
+
case "zai-thinking-disabled":
|
|
958
|
+
params.thinking = { type: "disabled" };
|
|
959
|
+
break;
|
|
960
|
+
case "qwen-enable-thinking-false":
|
|
961
|
+
params.enable_thinking = false;
|
|
962
|
+
break;
|
|
963
|
+
case "qwen-template-false":
|
|
964
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: false };
|
|
965
|
+
break;
|
|
966
|
+
case "openrouter-enabled-false":
|
|
967
|
+
(params as typeof params & { reasoning?: { effort?: string } | { enabled: false } }).reasoning = {
|
|
968
|
+
enabled: false,
|
|
969
|
+
};
|
|
970
|
+
break;
|
|
971
|
+
default:
|
|
972
|
+
delete params.reasoning;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void {
|
|
978
|
+
// `preserve_thinking` is a chat-template HISTORY knob, not a per-turn
|
|
979
|
+
// thinking switch — it controls whether OLDER assistant turns render
|
|
980
|
+
// with `<think>...</think>` on Qwen3.6+. Emit it BEFORE the reasoning
|
|
981
|
+
// state branches and EVERY early-return below, because the wire shape
|
|
982
|
+
// must carry the kwarg in three cases the auto-detected
|
|
983
|
+
// `qwenPreserveThinking` flag covers but `reasoning.enabled` does not:
|
|
984
|
+
//
|
|
985
|
+
// 1. Discovered local Qwen models. `discoverOpenAICompatibleModels`
|
|
986
|
+
// stamps `reasoning: false` on every spec built from a generic
|
|
987
|
+
// `/v1/models` endpoint (the upstream doesn't advertise the
|
|
988
|
+
// capability), so `model.reasoning === false` → `reasoning.enabled
|
|
989
|
+
// === false`, the body wouldn't otherwise see the kwarg, and the
|
|
990
|
+
// encoder's `replayReasoningContent` branch would keep shipping
|
|
991
|
+
// `reasoning_content` only for the template to strip `<think>` from
|
|
992
|
+
// older turns anyway. Exactly the #3528 / #3541 symptom on every
|
|
993
|
+
// discovered Qwen build.
|
|
994
|
+
// 2. Caller-disabled reasoning. The slot's KV cache still holds prior
|
|
995
|
+
// `<think>...</think>` tokens from earlier thinking turns; the
|
|
996
|
+
// template must keep rendering them or cache invalidates at the
|
|
997
|
+
// first historic `<think>`.
|
|
998
|
+
// 3. Forced-tool-choice / DeepSeek-style auto-disable. Same reasoning
|
|
999
|
+
// as (2) — historic thinking blocks have to survive history replay
|
|
1000
|
+
// even when the current turn cannot think.
|
|
1001
|
+
//
|
|
1002
|
+
// Non-Qwen templates ignore the parameter (jinja `is defined` check
|
|
1003
|
+
// silently no-ops), so emitting it unconditionally for the Qwen-family
|
|
1004
|
+
// + local-cache compat flag is safe.
|
|
1005
|
+
if (policy.compat.qwenPreserveThinking) {
|
|
1006
|
+
// Mirror the dialect split that gates `enable_thinking`. The
|
|
1007
|
+
// `qwen` dialect rides the top-level field (the only place
|
|
1008
|
+
// llama.cpp's `--jinja` hook AND Alibaba Cloud Model Studio's
|
|
1009
|
+
// compatible-mode look) while the `qwen-chat-template` dialect
|
|
1010
|
+
// (NVIDIA NIM, vLLM/SGLang's chat-template-kwargs path) MUST
|
|
1011
|
+
// ride only the kwargs copy — NIM's request schema is
|
|
1012
|
+
// `additionalProperties: false` and rejects every unknown
|
|
1013
|
+
// top-level field, the very reason `enable_thinking` is
|
|
1014
|
+
// route-split this way (#2299, see `catalog/src/compat/openai.ts`
|
|
1015
|
+
// thinkingFormat comment).
|
|
1016
|
+
if (policy.compat.thinkingFormat === "qwen") {
|
|
1017
|
+
params.preserve_thinking = true;
|
|
1018
|
+
}
|
|
1019
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, preserve_thinking: true };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const reasoning = policy.reasoning;
|
|
1023
|
+
if ((!reasoning.modelSupported && !reasoning.disabled) || !reasoning.supportsParams) return;
|
|
1024
|
+
if (reasoning.enabled) {
|
|
1025
|
+
switch (reasoning.disableMode) {
|
|
1026
|
+
case "zai-thinking-disabled":
|
|
1027
|
+
if (reasoning.wireEffort === "none") {
|
|
1028
|
+
encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (reasoning.dialect === "kimi" && reasoning.wireEffort !== undefined) {
|
|
1032
|
+
params.thinking = { type: "enabled", effort: reasoning.wireEffort };
|
|
1033
|
+
if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
params.thinking = { type: "enabled" };
|
|
1037
|
+
if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
|
|
1038
|
+
if (policy.compat.supportsReasoningEffort && reasoning.wireEffort !== undefined) {
|
|
1039
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
case "qwen-enable-thinking-false":
|
|
1043
|
+
params.enable_thinking = true;
|
|
1044
|
+
break;
|
|
1045
|
+
case "qwen-template-false":
|
|
1046
|
+
// Spread so the `preserve_thinking` kwarg hoisted above
|
|
1047
|
+
// survives the merge — a bare `{ enable_thinking: true }`
|
|
1048
|
+
// would clobber it.
|
|
1049
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: true };
|
|
1050
|
+
break;
|
|
1051
|
+
case "openrouter-enabled-false":
|
|
1052
|
+
if (reasoning.wireEffort !== undefined) {
|
|
1053
|
+
(params as typeof params & { reasoning?: { effort?: string } }).reasoning = {
|
|
1054
|
+
effort: reasoning.wireEffort,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
break;
|
|
1058
|
+
default:
|
|
1059
|
+
if (!reasoning.omitReasoningEffort && reasoning.wireEffort !== undefined) {
|
|
1060
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
1061
|
+
}
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (!reasoning.disabled) return;
|
|
1067
|
+
if (
|
|
1068
|
+
reasoning.disableReason === "caller" &&
|
|
1069
|
+
reasoning.requestedEffort === undefined &&
|
|
1070
|
+
reasoning.disableMode === "lowest-effort" &&
|
|
1071
|
+
reasoning.wireEffort !== undefined
|
|
1072
|
+
) {
|
|
1073
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export function applyChatCompletionsReasoningParams(
|
|
1080
|
+
params: OpenAICompletionsParams,
|
|
1081
|
+
model: Model<"openai-completions">,
|
|
1082
|
+
compat: ResolvedOpenAICompat,
|
|
1083
|
+
options: (ChatCompletionsReasoningOptions & { toolChoice?: unknown }) | undefined,
|
|
1084
|
+
): void {
|
|
1085
|
+
applyChatCompletionsCompatPolicy(
|
|
1086
|
+
params,
|
|
1087
|
+
resolveOpenAICompatPolicy(model, {
|
|
1088
|
+
endpoint: "chat-completions",
|
|
1089
|
+
compat,
|
|
1090
|
+
reasoning: options?.reasoning,
|
|
1091
|
+
disableReasoning: options?.disableReasoning,
|
|
1092
|
+
toolChoice: options?.toolChoice,
|
|
1093
|
+
}),
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export function disableChatCompletionsReasoningForDialect(
|
|
1098
|
+
params: OpenAICompletionsParams,
|
|
1099
|
+
compat: ResolvedOpenAICompat,
|
|
1100
|
+
): void {
|
|
1101
|
+
encodeChatCompletionsDisabledReasoning(params, compat.reasoningDisableMode);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Z.AI/GLM-5.2 reasoning-effort dialect predicate. GLM-5.2 models served on a
|
|
1106
|
+
* Z.AI-format host (thinkingFormat "zai") accept `reasoning_effort`, stream tool
|
|
1107
|
+
* calls via `tool_stream`, and clamp output to the model cap. Moonshot Kimi and
|
|
1108
|
+
* Xiaomi MiMo also resolve to thinkingFormat "zai" with supportsReasoningEffort
|
|
1109
|
+
* true but are NOT GLM-5.2, so the model-id check is load-bearing — never swap it
|
|
1110
|
+
* for `compat.supportsReasoningEffort`.
|
|
1111
|
+
*/
|
|
1112
|
+
function isZaiReasoningEffortDialect(model: Model<"openai-completions">, compat: ResolvedOpenAICompat): boolean {
|
|
1113
|
+
return compat.thinkingFormat === "zai" && isGlm52ReasoningEffortModelId(model.id);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Provider-specific Chat Completions output clamp.
|
|
1118
|
+
*
|
|
1119
|
+
* Most OpenAI-compatible endpoints retain the conservative 64k ceiling from
|
|
1120
|
+
* {@link resolveOpenAIOutputTokenParam}. Z.AI/GLM-5.2 reasoning and native
|
|
1121
|
+
* Moonshot K3 explicitly accept their full advertised model caps, so those
|
|
1122
|
+
* routes clamp to `model.maxTokens` instead.
|
|
1123
|
+
*/
|
|
1124
|
+
export function resolveOpenAICompletionsOutputClamp(
|
|
1125
|
+
model: Model<"openai-completions">,
|
|
1126
|
+
compat: ResolvedOpenAICompat,
|
|
1127
|
+
): number | undefined {
|
|
1128
|
+
if (isZaiReasoningEffortDialect(model, compat)) {
|
|
1129
|
+
return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
|
|
1130
|
+
}
|
|
1131
|
+
if (model.provider === "moonshot" && isKimiK3ModelId(model.id)) {
|
|
1132
|
+
return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
|
|
1133
|
+
}
|
|
1134
|
+
return undefined;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Provider-specific Responses API output clamp.
|
|
1139
|
+
*
|
|
1140
|
+
* Meta documents a 131,072-token output limit for Muse Spark 1.1, so native
|
|
1141
|
+
* Meta requests may use the model's full advertised cap instead of the
|
|
1142
|
+
* conservative 64k OpenAI-compatible default.
|
|
1143
|
+
*/
|
|
1144
|
+
export function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "provider" | "maxTokens">): number | undefined {
|
|
1145
|
+
if (model.provider === "meta") {
|
|
1146
|
+
return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
|
|
1147
|
+
}
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
|
|
1153
|
+
* (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
|
|
1154
|
+
*/
|
|
1155
|
+
export function applyChatCompletionsToolStream(
|
|
1156
|
+
params: OpenAICompletionsParams,
|
|
1157
|
+
model: Model<"openai-completions">,
|
|
1158
|
+
compat: ResolvedOpenAICompat,
|
|
1159
|
+
): void {
|
|
1160
|
+
if (
|
|
1161
|
+
isZaiReasoningEffortDialect(model, compat) &&
|
|
1162
|
+
compat.supportsReasoningEffort &&
|
|
1163
|
+
Array.isArray(params.tools) &&
|
|
1164
|
+
params.tools.length > 0
|
|
1165
|
+
) {
|
|
1166
|
+
params.tool_stream = true;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function isCompiledGrammarTooLargeStrictError(
|
|
1171
|
+
error: unknown,
|
|
1172
|
+
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1173
|
+
): boolean {
|
|
1174
|
+
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
|
1175
|
+
if (status !== 400) return false;
|
|
1176
|
+
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1177
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1178
|
+
.join("\n");
|
|
1179
|
+
return (
|
|
1180
|
+
/invalid_request_error/i.test(messageParts) &&
|
|
1181
|
+
/compiled grammar/i.test(messageParts) &&
|
|
1182
|
+
/too large/i.test(messageParts)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
interface StrictToolsRetryContext {
|
|
1187
|
+
model: OpenAIModelIdentity;
|
|
1188
|
+
strictToolsApplied: boolean;
|
|
1189
|
+
tools: Tool[] | undefined;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** Decide whether an OpenAI-family request should retry once with non-strict tools. */
|
|
1193
|
+
export function shouldRetryWithoutStrictTools(
|
|
1194
|
+
error: unknown,
|
|
1195
|
+
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1196
|
+
context: StrictToolsRetryContext,
|
|
1197
|
+
): boolean {
|
|
1198
|
+
const { model, strictToolsApplied, tools } = context;
|
|
1199
|
+
if (!tools || tools.length === 0 || !strictToolsApplied) return false;
|
|
1200
|
+
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
|
1201
|
+
if (status !== 400 && status !== 422) return false;
|
|
1202
|
+
const errorMessage = error instanceof Error ? error.message.trim() : "";
|
|
1203
|
+
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1204
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1205
|
+
.join("\n");
|
|
1206
|
+
if (
|
|
1207
|
+
/wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
|
|
1208
|
+
messageParts,
|
|
1209
|
+
)
|
|
1210
|
+
) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (model.provider !== "openrouter" || !/^(?:400\s+)?Provider returned error$/i.test(errorMessage)) return false;
|
|
1214
|
+
const body = capturedErrorResponse?.bodyJson;
|
|
1215
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
1216
|
+
const errorBody = body.error;
|
|
1217
|
+
if (errorBody && typeof errorBody === "object" && "metadata" in errorBody) {
|
|
1218
|
+
const metadata = errorBody.metadata;
|
|
1219
|
+
if (metadata && typeof metadata === "object" && "raw" in metadata) {
|
|
1220
|
+
const raw = metadata.raw;
|
|
1221
|
+
if (typeof raw === "string" ? raw.trim().length > 0 : raw != null) return false;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
|
|
1229
|
+
if (!value || value.length === 0) return undefined;
|
|
1230
|
+
const wellFormed = value.toWellFormed();
|
|
1231
|
+
if (wellFormed.length <= maxLength) return wellFormed;
|
|
1232
|
+
return `${hashPrefix}${Bun.hash(wellFormed).toString(36)}`;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
export const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
1236
|
+
"response.created",
|
|
1237
|
+
"response.output_item.added",
|
|
1238
|
+
"response.reasoning_summary_part.added",
|
|
1239
|
+
"response.reasoning_summary_text.delta",
|
|
1240
|
+
"response.reasoning_summary_text.done",
|
|
1241
|
+
"response.reasoning_summary_part.done",
|
|
1242
|
+
"response.reasoning_text.delta",
|
|
1243
|
+
"response.content_part.added",
|
|
1244
|
+
"response.output_text.delta",
|
|
1245
|
+
"response.refusal.delta",
|
|
1246
|
+
"response.function_call_arguments.delta",
|
|
1247
|
+
"response.function_call_arguments.done",
|
|
1248
|
+
"response.custom_tool_call_input.delta",
|
|
1249
|
+
"response.custom_tool_call_input.done",
|
|
1250
|
+
"response.output_item.done",
|
|
1251
|
+
"response.completed",
|
|
1252
|
+
"response.incomplete",
|
|
1253
|
+
"response.failed",
|
|
1254
|
+
"error",
|
|
1255
|
+
]);
|
|
1256
|
+
|
|
1257
|
+
export function isOpenAIResponsesProgressEvent(event: unknown): boolean {
|
|
1258
|
+
if (!event || typeof event !== "object") return false;
|
|
1259
|
+
const type = (event as { type?: unknown }).type;
|
|
1260
|
+
return typeof type === "string" && OPENAI_RESPONSES_PROGRESS_EVENT_TYPES.has(type);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string {
|
|
1264
|
+
const payload: TextSignatureV1 = { v: 1, id };
|
|
1265
|
+
if (phase) payload.phase = phase;
|
|
1266
|
+
return JSON.stringify(payload);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export function parseTextSignature(
|
|
1270
|
+
signature: string | undefined,
|
|
1271
|
+
): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
|
|
1272
|
+
if (!signature) return undefined;
|
|
1273
|
+
if (signature.startsWith("{")) {
|
|
1274
|
+
try {
|
|
1275
|
+
const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
|
|
1276
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
1277
|
+
if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
|
|
1278
|
+
return { id: parsed.id, phase: parsed.phase };
|
|
1279
|
+
}
|
|
1280
|
+
return { id: parsed.id };
|
|
1281
|
+
}
|
|
1282
|
+
} catch {
|
|
1283
|
+
// Fall through to legacy plain-string handling.
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return { id: signature };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export function encodeResponsesToolCallId(callId: string, itemId: string | null | undefined): string {
|
|
1290
|
+
const stableItemId = itemId && itemId.length > 0 ? itemId : `fc_${Bun.hash(callId).toString(36)}`;
|
|
1291
|
+
return `${callId}|${stableItemId}`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
export function normalizeResponsesToolCallIdForTransform(
|
|
1295
|
+
id: string,
|
|
1296
|
+
model?: Model<Api>,
|
|
1297
|
+
source?: AssistantMessage,
|
|
1298
|
+
): string {
|
|
1299
|
+
if (!id.includes("|")) return id;
|
|
1300
|
+
const isForeignToolCall =
|
|
1301
|
+
source != null && model != null && (source.provider !== model.provider || source.api !== model.api);
|
|
1302
|
+
if (isForeignToolCall) {
|
|
1303
|
+
const [callId, itemId] = id.split("|");
|
|
1304
|
+
const normalizeIdPart = (part: string): string => {
|
|
1305
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1306
|
+
const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
1307
|
+
return truncated.replace(/_+$/, "");
|
|
1308
|
+
};
|
|
1309
|
+
const normalizedCallId = normalizeIdPart(callId);
|
|
1310
|
+
let normalizedItemId = `fc_${Bun.hash(itemId).toString(36)}`;
|
|
1311
|
+
if (normalizedItemId.length > 64) normalizedItemId = normalizedItemId.slice(0, 64);
|
|
1312
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
1313
|
+
}
|
|
1314
|
+
const normalized = normalizeResponsesToolCallId(id);
|
|
1315
|
+
return `${normalized.callId}|${normalized.itemId}`;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
type ResponsesToolCallKind = "function" | "custom" | "computer";
|
|
1319
|
+
|
|
1320
|
+
function responsesToolCallKind(type: unknown): ResponsesToolCallKind | undefined {
|
|
1321
|
+
if (type === "function_call") return "function";
|
|
1322
|
+
if (type === "custom_tool_call") return "custom";
|
|
1323
|
+
if (type === "computer_call") return "computer";
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function responsesToolOutputKind(type: unknown): ResponsesToolCallKind | undefined {
|
|
1328
|
+
if (type === "function_call_output") return "function";
|
|
1329
|
+
if (type === "custom_tool_call_output") return "custom";
|
|
1330
|
+
if (type === "computer_call_output") return "computer";
|
|
1331
|
+
return undefined;
|
|
1332
|
+
}
|
|
1333
|
+
function responseInputCallId(item: ResponseInput[number]): string | undefined {
|
|
1334
|
+
if (!("call_id" in item)) return undefined;
|
|
1335
|
+
return typeof item.call_id === "string" ? item.call_id : undefined;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export function collectKnownCallIds(messages: ResponseInput): Set<string> {
|
|
1339
|
+
const knownCallIds = new Set<string>();
|
|
1340
|
+
for (const item of messages) {
|
|
1341
|
+
if (responsesToolCallKind(item.type) === undefined) continue;
|
|
1342
|
+
const callId = responseInputCallId(item);
|
|
1343
|
+
if (callId) knownCallIds.add(callId);
|
|
1344
|
+
}
|
|
1345
|
+
return knownCallIds;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** Scan replay items for call_ids that were originally custom tool calls. */
|
|
1349
|
+
export function collectCustomCallIds(messages: ResponseInput): Set<string> {
|
|
1350
|
+
const customCallIds = new Set<string>();
|
|
1351
|
+
for (const item of messages) {
|
|
1352
|
+
if (item.type !== "custom_tool_call") continue;
|
|
1353
|
+
const callId = responseInputCallId(item);
|
|
1354
|
+
if (callId) customCallIds.add(callId);
|
|
1355
|
+
}
|
|
1356
|
+
return customCallIds;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** Scan replay items for call_ids that were originally native computer calls. */
|
|
1360
|
+
export function collectComputerCallIds(messages: ResponseInput): Set<string> {
|
|
1361
|
+
const computerCallIds = new Set<string>();
|
|
1362
|
+
for (const item of messages) {
|
|
1363
|
+
if (item.type !== "computer_call") continue;
|
|
1364
|
+
const callId = responseInputCallId(item);
|
|
1365
|
+
if (callId) computerCallIds.add(callId);
|
|
1366
|
+
}
|
|
1367
|
+
return computerCallIds;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Convert orphan `function_call_output` / `custom_tool_call_output` items —
|
|
1372
|
+
* those whose `call_id` has no matching preceding `function_call` /
|
|
1373
|
+
* `custom_tool_call` in the same input — into assistant text notes.
|
|
1374
|
+
*
|
|
1375
|
+
* The Responses API rejects unpaired outputs with
|
|
1376
|
+
* `400 No tool call found for function call output with call_id …`. Orphans
|
|
1377
|
+
* sneak in through two paths today:
|
|
1378
|
+
*
|
|
1379
|
+
* - A previous turn's `providerPayload` snapshot replaces the input array via
|
|
1380
|
+
* the `dt: false` splice (see {@link convertConversationMessages}), wiping
|
|
1381
|
+
* the matching `function_call` while leaving the matching
|
|
1382
|
+
* `function_call_output` queued in a later `toolResult`.
|
|
1383
|
+
* - A locally-rejected tool call (argument-validation failure, hook reject,
|
|
1384
|
+
* aborted turn before the call streamed) produces a tool result without a
|
|
1385
|
+
* `function_call` ever landing in any persisted provider payload.
|
|
1386
|
+
*
|
|
1387
|
+
* Dropping the result loses information the model needs to recover; sending
|
|
1388
|
+
* it as-is 400s the request. Folding it into an assistant `message` preserves
|
|
1389
|
+
* the payload (call_id + truncated output) while staying within the Responses
|
|
1390
|
+
* input grammar. Matches the behavior of {@link transformRequestBody} in the
|
|
1391
|
+
* codex provider — issue #1351 / regression of #472.
|
|
1392
|
+
*/
|
|
1393
|
+
export function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput {
|
|
1394
|
+
const precedingCalls = new Set<string>();
|
|
1395
|
+
let repaired: ResponseInput | undefined;
|
|
1396
|
+
for (let index = 0; index < input.length; index++) {
|
|
1397
|
+
const item = input[index];
|
|
1398
|
+
const callKind = responsesToolCallKind(item.type);
|
|
1399
|
+
const callId = responseInputCallId(item);
|
|
1400
|
+
if (callKind && callId) precedingCalls.add(`${callKind}\0${callId}`);
|
|
1401
|
+
|
|
1402
|
+
const outputKind = responsesToolOutputKind(item.type);
|
|
1403
|
+
if (!outputKind || !callId || precedingCalls.has(`${outputKind}\0${callId}`)) {
|
|
1404
|
+
repaired?.push(item);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (!repaired) repaired = input.slice(0, index);
|
|
1409
|
+
const toolName = outputKind === "computer" ? "computer" : "tool";
|
|
1410
|
+
const rawOutput = "output" in item ? item.output : undefined;
|
|
1411
|
+
let text: string;
|
|
1412
|
+
if (typeof rawOutput === "string") text = rawOutput;
|
|
1413
|
+
else if (rawOutput == null) text = "";
|
|
1414
|
+
else {
|
|
1415
|
+
try {
|
|
1416
|
+
text = JSON.stringify(rawOutput);
|
|
1417
|
+
} catch {
|
|
1418
|
+
text = String(rawOutput);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const ORPHAN_OUTPUT_LIMIT = 16_000;
|
|
1422
|
+
if (text.length > ORPHAN_OUTPUT_LIMIT) text = `${text.slice(0, ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
1423
|
+
repaired.push({
|
|
1424
|
+
type: "message",
|
|
1425
|
+
role: "assistant",
|
|
1426
|
+
content: `[Orphan ${toolName} result; call_id=${callId}]: ${text}`,
|
|
1427
|
+
} as ResponseInput[number]);
|
|
1428
|
+
}
|
|
1429
|
+
return repaired ?? input;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Placeholder output for a tool call whose result is absent from the input. */
|
|
1433
|
+
const ORPHAN_TOOL_CALL_PLACEHOLDER =
|
|
1434
|
+
"[No tool output recorded: the tool call was interrupted before it produced a result.]";
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
|
|
1438
|
+
* for every `function_call` / `custom_tool_call` whose `call_id` has no matching
|
|
1439
|
+
* output later in the same input. The Responses API rejects an unpaired call
|
|
1440
|
+
* with `400 No tool output found for function call …`.
|
|
1441
|
+
*
|
|
1442
|
+
* Orphan calls surface when the user branches/navigates the session tree to a
|
|
1443
|
+
* node that ends on a tool call (the tool-result child is excluded from the
|
|
1444
|
+
* reconstructed history) or when a turn is aborted/crashes after the call
|
|
1445
|
+
* streamed but before its result persisted. Dropping the call would erase the
|
|
1446
|
+
* assistant's action; a placeholder output keeps the call visible so the model
|
|
1447
|
+
* can recover (e.g. re-issue the call). Symmetric to
|
|
1448
|
+
* {@link repairOrphanResponsesToolOutputs}.
|
|
1449
|
+
*/
|
|
1450
|
+
export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput {
|
|
1451
|
+
const laterOutputs = new Set<string>();
|
|
1452
|
+
const orphanIndexes = new Set<number>();
|
|
1453
|
+
for (let index = input.length - 1; index >= 0; index--) {
|
|
1454
|
+
const item = input[index];
|
|
1455
|
+
const callId = responseInputCallId(item);
|
|
1456
|
+
const outputKind = responsesToolOutputKind(item.type);
|
|
1457
|
+
if (outputKind && callId) laterOutputs.add(`${outputKind}\0${callId}`);
|
|
1458
|
+
|
|
1459
|
+
const callKind = responsesToolCallKind(item.type);
|
|
1460
|
+
if (callKind && callId && !laterOutputs.has(`${callKind}\0${callId}`)) orphanIndexes.add(index);
|
|
1461
|
+
}
|
|
1462
|
+
if (orphanIndexes.size === 0) return input;
|
|
1463
|
+
|
|
1464
|
+
const repaired: ResponseInput = [];
|
|
1465
|
+
for (let index = 0; index < input.length; index++) {
|
|
1466
|
+
const item = input[index];
|
|
1467
|
+
if (!orphanIndexes.has(index)) {
|
|
1468
|
+
repaired.push(item);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
const kind = responsesToolCallKind(item.type);
|
|
1472
|
+
const callId = responseInputCallId(item);
|
|
1473
|
+
if (!kind || !callId) {
|
|
1474
|
+
repaired.push(item);
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (kind === "computer") {
|
|
1478
|
+
repaired.push({
|
|
1479
|
+
type: "message",
|
|
1480
|
+
role: "assistant",
|
|
1481
|
+
content: `[Computer call interrupted before a screenshot was recorded; call_id=${callId}]`,
|
|
1482
|
+
} as ResponseInput[number]);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
repaired.push(item);
|
|
1486
|
+
repaired.push({
|
|
1487
|
+
type: kind === "custom" ? "custom_tool_call_output" : "function_call_output",
|
|
1488
|
+
call_id: callId,
|
|
1489
|
+
output: ORPHAN_TOOL_CALL_PLACEHOLDER,
|
|
1490
|
+
} as ResponseInput[number]);
|
|
1491
|
+
}
|
|
1492
|
+
return repaired;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Some Responses backends (notably GitHub Copilot) reject the OpenAI image
|
|
1497
|
+
* `detail: "original"` value with a 400. When the model does not advertise
|
|
1498
|
+
* support for it, degrade `"original"` to `"auto"` so the request still goes
|
|
1499
|
+
* through with the closest valid fidelity instead of failing outright. See #2822.
|
|
1500
|
+
*/
|
|
1501
|
+
function clampResponsesImageDetail(
|
|
1502
|
+
detail: ImageContent["detail"],
|
|
1503
|
+
supportsImageDetailOriginal: boolean,
|
|
1504
|
+
): ResponseInputImage["detail"] {
|
|
1505
|
+
const resolved = detail ?? "auto";
|
|
1506
|
+
return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
export function convertResponsesInputContent(
|
|
1510
|
+
content: string | Array<TextContent | ImageContent>,
|
|
1511
|
+
supportsImages: boolean,
|
|
1512
|
+
supportsImageDetailOriginal: boolean,
|
|
1513
|
+
escapeControlTokens = false,
|
|
1514
|
+
): ResponseInputContent[] | undefined {
|
|
1515
|
+
if (typeof content === "string") {
|
|
1516
|
+
if (content.trim().length === 0) return undefined;
|
|
1517
|
+
const text = content.toWellFormed();
|
|
1518
|
+
return [
|
|
1519
|
+
{
|
|
1520
|
+
type: "input_text",
|
|
1521
|
+
text: escapeControlTokens ? escapeHarmonyControlTokens(text) : text,
|
|
1522
|
+
} satisfies ResponseInputText,
|
|
1523
|
+
];
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const { textBlocks, imageBlocks, omittedImages } = partitionVisionContent(content, supportsImages);
|
|
1527
|
+
const normalizedContent: ResponseInputContent[] = [];
|
|
1528
|
+
for (const item of textBlocks) {
|
|
1529
|
+
const raw = item.text.toWellFormed();
|
|
1530
|
+
const text = escapeControlTokens ? escapeHarmonyControlTokens(raw) : raw;
|
|
1531
|
+
if (text.trim().length === 0) continue;
|
|
1532
|
+
normalizedContent.push({
|
|
1533
|
+
type: "input_text",
|
|
1534
|
+
text,
|
|
1535
|
+
} satisfies ResponseInputText);
|
|
1536
|
+
}
|
|
1537
|
+
for (const item of imageBlocks) {
|
|
1538
|
+
normalizedContent.push({
|
|
1539
|
+
type: "input_image",
|
|
1540
|
+
detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
|
|
1541
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
1542
|
+
} satisfies ResponseInputImage);
|
|
1543
|
+
}
|
|
1544
|
+
if (omittedImages) {
|
|
1545
|
+
normalizedContent.push({
|
|
1546
|
+
type: "input_text",
|
|
1547
|
+
text: NON_VISION_IMAGE_PLACEHOLDER,
|
|
1548
|
+
} satisfies ResponseInputText);
|
|
1549
|
+
}
|
|
1550
|
+
return normalizedContent.length > 0 ? normalizedContent : undefined;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Map freeform custom-tool wire names back to the internal tool name for
|
|
1555
|
+
* providers that only accept function_call / function_call_output.
|
|
1556
|
+
* Built once per request; `apply_patch` → `edit` is the OMP default.
|
|
1557
|
+
*/
|
|
1558
|
+
function buildCustomToolWireNameMap(tools: readonly Tool[] | undefined): ReadonlyMap<string, string> | undefined {
|
|
1559
|
+
if (!tools?.length) return undefined;
|
|
1560
|
+
const map = new Map<string, string>();
|
|
1561
|
+
for (const tool of tools) {
|
|
1562
|
+
if (tool.customWireName) map.set(tool.customWireName, tool.name);
|
|
1563
|
+
}
|
|
1564
|
+
return map.size > 0 ? map : undefined;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function resolveReplayCustomToolName(wireName: string, wireNameMap: ReadonlyMap<string, string> | undefined): string {
|
|
1568
|
+
return wireNameMap?.get(wireName) ?? (wireName === "apply_patch" ? "edit" : wireName);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Downgrade OpenAI-only custom tool items when the target model does not
|
|
1573
|
+
* advertise freeform custom tools (`applyPatchToolType === "freeform"`).
|
|
1574
|
+
* No-op (returns the same array reference) when freeform is supported.
|
|
1575
|
+
*/
|
|
1576
|
+
function adaptResponsesReplayItemsForModel(
|
|
1577
|
+
input: ResponseInput,
|
|
1578
|
+
supportsCustomToolCalls: boolean,
|
|
1579
|
+
wireNameMap: ReadonlyMap<string, string> | undefined,
|
|
1580
|
+
supportsComputerUse: boolean,
|
|
1581
|
+
): ResponseInput {
|
|
1582
|
+
if (supportsCustomToolCalls && supportsComputerUse) return input;
|
|
1583
|
+
|
|
1584
|
+
let changed = false;
|
|
1585
|
+
const adapted: ResponseInput = [];
|
|
1586
|
+
for (const item of input) {
|
|
1587
|
+
if (!supportsCustomToolCalls && item.type === "custom_tool_call") {
|
|
1588
|
+
changed = true;
|
|
1589
|
+
adapted.push({
|
|
1590
|
+
type: "function_call",
|
|
1591
|
+
...(item.id ? { id: item.id } : {}),
|
|
1592
|
+
call_id: item.call_id,
|
|
1593
|
+
name: resolveReplayCustomToolName(item.name, wireNameMap),
|
|
1594
|
+
arguments: JSON.stringify({ input: item.input }),
|
|
1595
|
+
...(item.namespace ? { namespace: item.namespace } : {}),
|
|
1596
|
+
});
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
if (!supportsCustomToolCalls && item.type === "custom_tool_call_output") {
|
|
1600
|
+
changed = true;
|
|
1601
|
+
adapted.push({
|
|
1602
|
+
type: "function_call_output",
|
|
1603
|
+
call_id: item.call_id,
|
|
1604
|
+
output: item.output,
|
|
1605
|
+
});
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (!supportsComputerUse && (item.type === "computer_call" || item.type === "computer_call_output")) {
|
|
1609
|
+
changed = true;
|
|
1610
|
+
const callId = responseInputCallId(item) ?? "unknown";
|
|
1611
|
+
adapted.push({
|
|
1612
|
+
type: "message",
|
|
1613
|
+
role: "assistant",
|
|
1614
|
+
content: `[Previous computer ${item.type === "computer_call" ? "call" : "result"}; call_id=${callId}]: ${stringifyJson(item) ?? ""}`,
|
|
1615
|
+
} as ResponseInput[number]);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
adapted.push(item);
|
|
1619
|
+
}
|
|
1620
|
+
return changed ? adapted : input;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
1624
|
+
model: Model<TApi>;
|
|
1625
|
+
context: Context;
|
|
1626
|
+
strictResponsesPairing: boolean;
|
|
1627
|
+
supportsImageDetailOriginal: boolean;
|
|
1628
|
+
systemRole?: "system" | "developer";
|
|
1629
|
+
nativeHistory?: {
|
|
1630
|
+
replay: boolean;
|
|
1631
|
+
filterReasoning: boolean;
|
|
1632
|
+
};
|
|
1633
|
+
includeThinkingSignatures?: boolean;
|
|
1634
|
+
developerStringContent?: boolean;
|
|
1635
|
+
repairOrphanOutputs?: boolean;
|
|
1636
|
+
/** Preserve assistant message item IDs from text signatures during fallback replay. */
|
|
1637
|
+
preserveAssistantMessageIds?: boolean;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Escape reserved Harmony control tokens in the free-text fields of replayed
|
|
1642
|
+
* Responses input items: user/developer/system text, tool-result output,
|
|
1643
|
+
* assistant message text, and tool-call payloads.
|
|
1644
|
+
*
|
|
1645
|
+
* Tool-call items are covered deliberately. The original #6913 fix skipped
|
|
1646
|
+
* model-owned items on the theory that they carry no client data — but a model
|
|
1647
|
+
* legitimately writing *about* Harmony samples `<|channel|>` etc. into its own
|
|
1648
|
+
* `function_call.arguments`, and a full-transcript replay (stale or blocked
|
|
1649
|
+
* previous_response_id, provider fallback) feeds those bytes back as input,
|
|
1650
|
+
* which gpt-5.x reject with invalid_prompt / "Request blocked", permanently
|
|
1651
|
+
* poisoning the session. `arguments` is a JSON document, so it uses
|
|
1652
|
+
* {@link escapeHarmonyControlTokensInJson} to stay parseable. Reasoning items
|
|
1653
|
+
* are left untouched: `encrypted_content` is opaque and plaintext summaries
|
|
1654
|
+
* are never rendered back into the prompt.
|
|
1655
|
+
*
|
|
1656
|
+
* Native history replay pushes stored `providerPayload` items straight onto the
|
|
1657
|
+
* wire, bypassing {@link convertResponsesInputContent}; without this a stored
|
|
1658
|
+
* `input_text` carrying `<|channel|>analysis` still reaches gpt-5.x raw (#6913).
|
|
1659
|
+
* Callers gate on {@link isHarmonyDialectModel}. Items are copied, not mutated.
|
|
1660
|
+
*/
|
|
1661
|
+
export function escapeReplayedControlTokens(items: ResponseInput): ResponseInput {
|
|
1662
|
+
return items.map(item => {
|
|
1663
|
+
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
1664
|
+
return typeof item.output === "string" ? { ...item, output: escapeHarmonyControlTokens(item.output) } : item;
|
|
1665
|
+
}
|
|
1666
|
+
if (item.type === "function_call") {
|
|
1667
|
+
return typeof item.arguments === "string"
|
|
1668
|
+
? { ...item, arguments: escapeHarmonyControlTokensInJson(item.arguments) }
|
|
1669
|
+
: item;
|
|
1670
|
+
}
|
|
1671
|
+
if (item.type === "custom_tool_call") {
|
|
1672
|
+
return typeof item.input === "string" ? { ...item, input: escapeHarmonyControlTokens(item.input) } : item;
|
|
1673
|
+
}
|
|
1674
|
+
// EasyInputMessage may omit `type` (`{ role, content }`); the responses
|
|
1675
|
+
// server persists it verbatim, so treat missing type as a message too.
|
|
1676
|
+
const isTypedMessage = item.type === "message" || item.type === undefined;
|
|
1677
|
+
if (!isTypedMessage || !("role" in item) || !("content" in item)) return item;
|
|
1678
|
+
if (item.role === "assistant") {
|
|
1679
|
+
// Assistant output text is model-owned but equally capable of carrying
|
|
1680
|
+
// control tokens as data. `status` discriminates ResponseOutputMessage.
|
|
1681
|
+
if ("status" in item && Array.isArray(item.content)) {
|
|
1682
|
+
return {
|
|
1683
|
+
...item,
|
|
1684
|
+
content: item.content.map(part =>
|
|
1685
|
+
part.type === "output_text"
|
|
1686
|
+
? { ...part, text: escapeHarmonyControlTokens(part.text) }
|
|
1687
|
+
: part.type === "refusal"
|
|
1688
|
+
? { ...part, refusal: escapeHarmonyControlTokens(part.refusal) }
|
|
1689
|
+
: part,
|
|
1690
|
+
),
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
return item;
|
|
1694
|
+
}
|
|
1695
|
+
const content = item.content;
|
|
1696
|
+
if (typeof content === "string") {
|
|
1697
|
+
return { ...item, content: escapeHarmonyControlTokens(content) };
|
|
1698
|
+
}
|
|
1699
|
+
if (Array.isArray(content)) {
|
|
1700
|
+
return {
|
|
1701
|
+
...item,
|
|
1702
|
+
content: content.map(part =>
|
|
1703
|
+
part.type === "input_text" ? { ...part, text: escapeHarmonyControlTokens(part.text) } : part,
|
|
1704
|
+
),
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
return item;
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput {
|
|
1712
|
+
const messages: ResponseInput = [];
|
|
1713
|
+
const systemPrompts = options.systemRole ? normalizeSystemPrompts(options.context.systemPrompt) : [];
|
|
1714
|
+
for (const systemPrompt of systemPrompts) {
|
|
1715
|
+
messages.push({ role: options.systemRole as "system" | "developer", content: systemPrompt });
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Compat is resolved by the catalog (e.g. Copilot / xai-oauth reject
|
|
1719
|
+
// `detail: "original"`). Do not re-branch on provider id here.
|
|
1720
|
+
const supportsImageDetailOriginal = options.supportsImageDetailOriginal;
|
|
1721
|
+
// Freeform custom tools (`custom_tool_call`) only when the catalog says so;
|
|
1722
|
+
// same gate as tool conversion (`applyPatchToolType === "freeform"`).
|
|
1723
|
+
const supportsCustomToolCalls = options.model.applyPatchToolType === "freeform";
|
|
1724
|
+
const customToolWireNameMap = supportsCustomToolCalls
|
|
1725
|
+
? undefined
|
|
1726
|
+
: buildCustomToolWireNameMap(options.context.tools);
|
|
1727
|
+
let knownCallIds = new Set<string>();
|
|
1728
|
+
const customCallIds = new Set<string>();
|
|
1729
|
+
const computerCallIds = new Set<string>();
|
|
1730
|
+
const transformedMessages = transformMessages(
|
|
1731
|
+
options.context.messages,
|
|
1732
|
+
options.model,
|
|
1733
|
+
normalizeResponsesToolCallIdForTransform,
|
|
1734
|
+
);
|
|
1735
|
+
const filterReasoning = <T extends { type?: string }>(items: T[]): T[] =>
|
|
1736
|
+
options.nativeHistory?.filterReasoning ? items.filter(item => item?.type !== "reasoning") : items;
|
|
1737
|
+
const includeThinkingSignatures = options.includeThinkingSignatures ?? options.nativeHistory?.replay ?? true;
|
|
1738
|
+
// Harmony-server models (gpt-5.x) reject requests whose input data reproduces
|
|
1739
|
+
// reserved control-token spellings; escape the transport copy of untrusted
|
|
1740
|
+
// user/tool text so ordinary docs, code, or grep results cannot poison the
|
|
1741
|
+
// session (#6913). The persisted transcript is never touched.
|
|
1742
|
+
const escapeControlTokens = isHarmonyDialectModel(options.model);
|
|
1743
|
+
|
|
1744
|
+
let msgIndex = 0;
|
|
1745
|
+
for (const msg of transformedMessages) {
|
|
1746
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
1747
|
+
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
1748
|
+
const historyItems = options.nativeHistory
|
|
1749
|
+
? getOpenAIResponsesHistoryItems(providerPayload, options.model.provider)
|
|
1750
|
+
: undefined;
|
|
1751
|
+
const shouldReplayPayloadItems =
|
|
1752
|
+
options.nativeHistory?.replay ||
|
|
1753
|
+
(historyItems?.some(item => {
|
|
1754
|
+
if (!item || typeof item !== "object") return false;
|
|
1755
|
+
const candidate = item as { type?: unknown };
|
|
1756
|
+
return candidate.type === "compaction" || candidate.type === "compaction_summary";
|
|
1757
|
+
}) ??
|
|
1758
|
+
false);
|
|
1759
|
+
if (historyItems && shouldReplayPayloadItems) {
|
|
1760
|
+
const sanitizedItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems), {
|
|
1761
|
+
supportsImageDetailOriginal,
|
|
1762
|
+
supportsComputerUse: options.model.supportsComputerUse === true,
|
|
1763
|
+
});
|
|
1764
|
+
const replayItems = adaptResponsesReplayItemsForModel(
|
|
1765
|
+
sanitizedItems,
|
|
1766
|
+
supportsCustomToolCalls,
|
|
1767
|
+
customToolWireNameMap,
|
|
1768
|
+
options.model.supportsComputerUse === true,
|
|
1769
|
+
);
|
|
1770
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
|
|
1771
|
+
knownCallIds = collectKnownCallIds(messages);
|
|
1772
|
+
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1773
|
+
for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
|
|
1774
|
+
msgIndex++;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
const content = convertResponsesInputContent(
|
|
1778
|
+
msg.content,
|
|
1779
|
+
options.model.input.includes("image"),
|
|
1780
|
+
supportsImageDetailOriginal,
|
|
1781
|
+
escapeControlTokens,
|
|
1782
|
+
);
|
|
1783
|
+
if (!content) continue;
|
|
1784
|
+
const developerText =
|
|
1785
|
+
options.developerStringContent && msg.role === "developer" && typeof msg.content === "string"
|
|
1786
|
+
? msg.content.toWellFormed()
|
|
1787
|
+
: undefined;
|
|
1788
|
+
messages.push({
|
|
1789
|
+
role: "user",
|
|
1790
|
+
content:
|
|
1791
|
+
developerText !== undefined
|
|
1792
|
+
? escapeControlTokens
|
|
1793
|
+
? escapeHarmonyControlTokens(developerText)
|
|
1794
|
+
: developerText
|
|
1795
|
+
: content,
|
|
1796
|
+
});
|
|
1797
|
+
} else if (msg.role === "assistant") {
|
|
1798
|
+
const assistantMsg = msg as AssistantMessage;
|
|
1799
|
+
// Providers replay stale native items even when the current request has
|
|
1800
|
+
// disabled native replay (cold session state, filter policy). Consult
|
|
1801
|
+
// the payload sanitizer directly so hidden-empty turns are recognized
|
|
1802
|
+
// on both the warm and cold paths.
|
|
1803
|
+
const providerPayload =
|
|
1804
|
+
assistantMsg.api === options.model.api && assistantMsg.model === options.model.id
|
|
1805
|
+
? getOpenAIResponsesHistoryPayload(
|
|
1806
|
+
assistantMsg.providerPayload,
|
|
1807
|
+
options.model.provider,
|
|
1808
|
+
assistantMsg.provider,
|
|
1809
|
+
)
|
|
1810
|
+
: undefined;
|
|
1811
|
+
const nativeReplayEnabled = options.nativeHistory?.replay === true;
|
|
1812
|
+
const historyItems = providerPayload?.items;
|
|
1813
|
+
let suppressHiddenEmptyFallback = false;
|
|
1814
|
+
if (historyItems) {
|
|
1815
|
+
const rawSanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
1816
|
+
filterReasoning(historyItems),
|
|
1817
|
+
{
|
|
1818
|
+
supportsImageDetailOriginal,
|
|
1819
|
+
supportsComputerUse: options.model.supportsComputerUse === true,
|
|
1820
|
+
},
|
|
1821
|
+
);
|
|
1822
|
+
const sanitizedHistoryItems = rawSanitizedHistoryItems
|
|
1823
|
+
? adaptResponsesReplayItemsForModel(
|
|
1824
|
+
rawSanitizedHistoryItems,
|
|
1825
|
+
supportsCustomToolCalls,
|
|
1826
|
+
customToolWireNameMap,
|
|
1827
|
+
options.model.supportsComputerUse === true,
|
|
1828
|
+
)
|
|
1829
|
+
: undefined;
|
|
1830
|
+
if (nativeReplayEnabled && sanitizedHistoryItems) {
|
|
1831
|
+
// Model-owned replay items can carry reserved control-token
|
|
1832
|
+
// spellings as data (the model writing *about* Harmony); escape the
|
|
1833
|
+
// transport copy just like client turns.
|
|
1834
|
+
const wireItems = escapeControlTokens
|
|
1835
|
+
? escapeReplayedControlTokens(sanitizedHistoryItems)
|
|
1836
|
+
: sanitizedHistoryItems;
|
|
1837
|
+
if (providerPayload?.dt) {
|
|
1838
|
+
messages.push(...wireItems);
|
|
1839
|
+
} else {
|
|
1840
|
+
messages.splice(0, messages.length, ...wireItems);
|
|
1841
|
+
customCallIds.clear();
|
|
1842
|
+
computerCallIds.clear();
|
|
1843
|
+
}
|
|
1844
|
+
knownCallIds = collectKnownCallIds(messages);
|
|
1845
|
+
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1846
|
+
for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
|
|
1847
|
+
msgIndex++;
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (!sanitizedHistoryItems) suppressHiddenEmptyFallback = true;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const convertedOutputItems = convertResponsesAssistantMessage(
|
|
1854
|
+
assistantMsg,
|
|
1855
|
+
options.model,
|
|
1856
|
+
msgIndex,
|
|
1857
|
+
knownCallIds,
|
|
1858
|
+
suppressHiddenEmptyFallback ? false : includeThinkingSignatures,
|
|
1859
|
+
customCallIds,
|
|
1860
|
+
options.preserveAssistantMessageIds,
|
|
1861
|
+
supportsCustomToolCalls,
|
|
1862
|
+
customToolWireNameMap,
|
|
1863
|
+
computerCallIds,
|
|
1864
|
+
);
|
|
1865
|
+
const outputItems = suppressHiddenEmptyFallback
|
|
1866
|
+
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
|
|
1867
|
+
: convertedOutputItems;
|
|
1868
|
+
if (outputItems.length === 0) continue;
|
|
1869
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
|
|
1870
|
+
} else if (msg.role === "toolResult") {
|
|
1871
|
+
appendResponsesToolResultMessages(
|
|
1872
|
+
messages,
|
|
1873
|
+
msg,
|
|
1874
|
+
options.model,
|
|
1875
|
+
options.strictResponsesPairing,
|
|
1876
|
+
supportsImageDetailOriginal,
|
|
1877
|
+
knownCallIds,
|
|
1878
|
+
customCallIds,
|
|
1879
|
+
supportsCustomToolCalls,
|
|
1880
|
+
computerCallIds,
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
msgIndex++;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const withRepairedOutputs = options.repairOrphanOutputs ? repairOrphanResponsesToolOutputs(messages) : messages;
|
|
1887
|
+
const withRepairedCalls = repairOrphanResponsesToolCalls(withRepairedOutputs);
|
|
1888
|
+
return stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(withRepairedCalls);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
|
|
1892
|
+
|
|
1893
|
+
function parseResponseReasoningReplayItem(signature: string | undefined): ResponseReasoningItem | undefined {
|
|
1894
|
+
if (!signature) return undefined;
|
|
1895
|
+
try {
|
|
1896
|
+
const parsed = JSON.parse(signature) as unknown;
|
|
1897
|
+
if (!parsed || typeof parsed !== "object") return undefined;
|
|
1898
|
+
if (!("type" in parsed) || parsed.type !== "reasoning") return undefined;
|
|
1899
|
+
if (!("id" in parsed) || typeof parsed.id !== "string") return undefined;
|
|
1900
|
+
return parsed as ResponseReasoningItem;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return undefined;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
1907
|
+
assistantMsg: AssistantMessage,
|
|
1908
|
+
model: Model<TApi>,
|
|
1909
|
+
msgIndex: number,
|
|
1910
|
+
knownCallIds: Set<string>,
|
|
1911
|
+
includeThinkingSignatures = true,
|
|
1912
|
+
customCallIds?: Set<string>,
|
|
1913
|
+
preserveMessageIds = false,
|
|
1914
|
+
supportsCustomToolCalls = true,
|
|
1915
|
+
customToolWireNameMap?: ReadonlyMap<string, string>,
|
|
1916
|
+
computerCallIds?: Set<string>,
|
|
1917
|
+
): ResponseInput {
|
|
1918
|
+
const outputItems: ResponseInput = [];
|
|
1919
|
+
let unsignedTextBlocks = 0;
|
|
1920
|
+
const hasReplayableReasoningItem =
|
|
1921
|
+
includeThinkingSignatures &&
|
|
1922
|
+
assistantMsg.stopReason !== "error" &&
|
|
1923
|
+
assistantMsg.content.some(
|
|
1924
|
+
block => block.type === "thinking" && parseResponseReasoningReplayItem(block.thinkingSignature) !== undefined,
|
|
1925
|
+
);
|
|
1926
|
+
const isDifferentModel =
|
|
1927
|
+
assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
1928
|
+
|
|
1929
|
+
for (const block of assistantMsg.content) {
|
|
1930
|
+
if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
|
|
1931
|
+
if (!includeThinkingSignatures) {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
|
|
1935
|
+
if (reasoningItem) outputItems.push(reasoningItem);
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (block.type === "text") {
|
|
1940
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
1941
|
+
let msgId = parsedSignature?.id;
|
|
1942
|
+
if (!msgId) {
|
|
1943
|
+
if (hasReplayableReasoningItem) {
|
|
1944
|
+
// Distinct ids per unsigned block: several text blocks in one message
|
|
1945
|
+
// (cross-provider replay downgrades thinking → text) must not share an id.
|
|
1946
|
+
msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
|
|
1947
|
+
unsignedTextBlocks += 1;
|
|
1948
|
+
}
|
|
1949
|
+
} else if (!preserveMessageIds && !hasReplayableReasoningItem) {
|
|
1950
|
+
// Without the matching reasoning item the server rejects replayed
|
|
1951
|
+
// item ids (#4173) — drop them regardless of shape, including
|
|
1952
|
+
// legacy plain-string signatures that would otherwise fall into
|
|
1953
|
+
// the >64-char hash branch and fabricate a bogus msg_ id.
|
|
1954
|
+
msgId = undefined;
|
|
1955
|
+
} else if (msgId.length > 64) {
|
|
1956
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
1957
|
+
}
|
|
1958
|
+
const messageItem: ResponsesReplayAssistantMessage = {
|
|
1959
|
+
type: "message",
|
|
1960
|
+
role: "assistant",
|
|
1961
|
+
content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
|
|
1962
|
+
status: "completed",
|
|
1963
|
+
...(msgId ? { id: msgId } : {}),
|
|
1964
|
+
...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
|
|
1965
|
+
};
|
|
1966
|
+
outputItems.push(messageItem as ResponseInput[number]);
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (block.type !== "toolCall") {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (block.providerMetadata?.type === "computer") {
|
|
1975
|
+
if (model.supportsComputerUse !== true) {
|
|
1976
|
+
const callId = normalizeResponsesToolCallId(block.id, "ctc").callId;
|
|
1977
|
+
outputItems.push({
|
|
1978
|
+
type: "message",
|
|
1979
|
+
role: "assistant",
|
|
1980
|
+
content: `[Previous computer call; call_id=${callId}]: ${stringifyJson(block.providerMetadata.actions) ?? ""}`,
|
|
1981
|
+
} as ResponseInput[number]);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
const normalized = normalizeResponsesToolCallId(block.id, "ctc");
|
|
1985
|
+
knownCallIds.add(normalized.callId);
|
|
1986
|
+
computerCallIds?.add(normalized.callId);
|
|
1987
|
+
outputItems.push({
|
|
1988
|
+
type: "computer_call",
|
|
1989
|
+
id: block.providerMetadata.providerItemId,
|
|
1990
|
+
call_id: normalized.callId,
|
|
1991
|
+
actions: structuredCloneJSON(block.providerMetadata.actions),
|
|
1992
|
+
pending_safety_checks: structuredCloneJSON(block.providerMetadata.pendingSafetyChecks),
|
|
1993
|
+
status: "completed",
|
|
1994
|
+
} as ResponseInput[number]);
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
|
|
1998
|
+
let itemId: string | undefined = normalized.itemId;
|
|
1999
|
+
if (
|
|
2000
|
+
!hasReplayableReasoningItem &&
|
|
2001
|
+
(itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
|
|
2002
|
+
) {
|
|
2003
|
+
itemId = undefined;
|
|
2004
|
+
} else if (
|
|
2005
|
+
isDifferentModel &&
|
|
2006
|
+
(itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
|
|
2007
|
+
) {
|
|
2008
|
+
itemId = undefined;
|
|
2009
|
+
}
|
|
2010
|
+
knownCallIds.add(normalized.callId);
|
|
2011
|
+
if (block.customWireName && supportsCustomToolCalls) {
|
|
2012
|
+
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
|
|
2013
|
+
customCallIds?.add(normalized.callId);
|
|
2014
|
+
outputItems.push({
|
|
2015
|
+
type: "custom_tool_call",
|
|
2016
|
+
...(itemId ? { id: itemId } : {}),
|
|
2017
|
+
call_id: normalized.callId,
|
|
2018
|
+
name: block.customWireName,
|
|
2019
|
+
input: rawInput,
|
|
2020
|
+
} as ResponseInput[number]);
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const functionName =
|
|
2024
|
+
block.customWireName && !supportsCustomToolCalls
|
|
2025
|
+
? resolveReplayCustomToolName(block.customWireName, customToolWireNameMap)
|
|
2026
|
+
: block.name;
|
|
2027
|
+
outputItems.push({
|
|
2028
|
+
type: "function_call",
|
|
2029
|
+
...(itemId ? { id: itemId } : {}),
|
|
2030
|
+
call_id: normalized.callId,
|
|
2031
|
+
name: functionName,
|
|
2032
|
+
arguments: stringifyJson(block.arguments) ?? "null",
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
return outputItems;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
const syntheticToolImageMessages = new WeakSet<object>();
|
|
2040
|
+
|
|
2041
|
+
function insertResponsesToolOutput(messages: ResponseInput, output: ResponseInput[number]): void {
|
|
2042
|
+
let index = messages.length;
|
|
2043
|
+
while (index > 0) {
|
|
2044
|
+
const previous = messages[index - 1];
|
|
2045
|
+
if (typeof previous !== "object" || previous === null || !syntheticToolImageMessages.has(previous)) {
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
index -= 1;
|
|
2049
|
+
}
|
|
2050
|
+
messages.splice(index, 0, output);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/** Appends one tool result while keeping consecutive outputs ahead of its synthetic image messages. */
|
|
2054
|
+
export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
2055
|
+
messages: ResponseInput,
|
|
2056
|
+
toolResult: ToolResultMessage,
|
|
2057
|
+
model: Model<TApi>,
|
|
2058
|
+
strictResponsesPairing: boolean,
|
|
2059
|
+
supportsImageDetailOriginal: boolean,
|
|
2060
|
+
knownCallIds: ReadonlySet<string>,
|
|
2061
|
+
customCallIds?: ReadonlySet<string>,
|
|
2062
|
+
supportsCustomToolCalls = true,
|
|
2063
|
+
computerCallIds?: ReadonlySet<string>,
|
|
2064
|
+
): void {
|
|
2065
|
+
const supportsImages = model.input.includes("image");
|
|
2066
|
+
const textResult = toolResult.content
|
|
2067
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
2068
|
+
.map(block => block.text)
|
|
2069
|
+
.join("\n");
|
|
2070
|
+
const hasImages = toolResult.content.some((block): block is ImageContent => block.type === "image");
|
|
2071
|
+
const omittedImages = hasImages && !supportsImages;
|
|
2072
|
+
const normalized = normalizeResponsesToolCallId(toolResult.toolCallId);
|
|
2073
|
+
// "(see attached image)" is only truthful when the result actually carries
|
|
2074
|
+
// images (they ride as a separate user message on the Responses API). A
|
|
2075
|
+
// genuinely empty text result (empty file read, silent tool) must stay
|
|
2076
|
+
// empty — the placeholder sent models chasing an attachment that never
|
|
2077
|
+
// existed.
|
|
2078
|
+
const rawOutput = (
|
|
2079
|
+
omittedImages
|
|
2080
|
+
? joinTextWithImagePlaceholder(textResult, true)
|
|
2081
|
+
: textResult.length > 0
|
|
2082
|
+
? textResult
|
|
2083
|
+
: hasImages
|
|
2084
|
+
? "(see attached image)"
|
|
2085
|
+
: ""
|
|
2086
|
+
).toWellFormed();
|
|
2087
|
+
// Harmony-server models reject reserved control-token spellings even as tool
|
|
2088
|
+
// data; escape the transport copy so a grep/read result cannot poison the
|
|
2089
|
+
// session (#6913). Covers every downstream branch that consumes `output`.
|
|
2090
|
+
const output = isHarmonyDialectModel(model) ? escapeHarmonyControlTokens(rawOutput) : rawOutput;
|
|
2091
|
+
if (toolResult.providerMetadata?.type === "computer" && model.supportsComputerUse !== true) {
|
|
2092
|
+
messages.push({
|
|
2093
|
+
type: "message",
|
|
2094
|
+
role: "assistant",
|
|
2095
|
+
content: `[Previous computer result; call_id=${normalized.callId}]: ${stringifyJson(toolResult.providerMetadata.screenshot) ?? ""}`,
|
|
2096
|
+
} as ResponseInput[number]);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (computerCallIds?.has(normalized.callId)) {
|
|
2100
|
+
if (toolResult.providerMetadata?.type !== "computer") {
|
|
2101
|
+
const limit = 16_000;
|
|
2102
|
+
const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
|
|
2103
|
+
messages.push({
|
|
2104
|
+
type: "message",
|
|
2105
|
+
role: "assistant",
|
|
2106
|
+
content: `[Computer tool failed before a screenshot was produced; call_id=${normalized.callId}]: ${noteText}`,
|
|
2107
|
+
} as ResponseInput[number]);
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
|
|
2111
|
+
messages.push({
|
|
2112
|
+
type: "message",
|
|
2113
|
+
role: "assistant",
|
|
2114
|
+
content: `[Orphan computer result; call_id=${normalized.callId}]`,
|
|
2115
|
+
} as ResponseInput[number]);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
insertResponsesToolOutput(messages, {
|
|
2119
|
+
type: "computer_call_output",
|
|
2120
|
+
call_id: normalized.callId,
|
|
2121
|
+
output: structuredCloneJSON(toolResult.providerMetadata.screenshot),
|
|
2122
|
+
acknowledged_safety_checks: structuredCloneJSON(toolResult.providerMetadata.acknowledgedSafetyChecks),
|
|
2123
|
+
} as ResponseInput[number]);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
|
|
2127
|
+
// Strict backends (Azure, Copilot) reject unpaired outputs outright, but
|
|
2128
|
+
// silently dropping the result loses information the model needs. Fold it
|
|
2129
|
+
// into an assistant note instead (same shape as repairOrphanResponsesToolOutputs).
|
|
2130
|
+
const limit = 16_000;
|
|
2131
|
+
const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
|
|
2132
|
+
messages.push({
|
|
2133
|
+
type: "message",
|
|
2134
|
+
role: "assistant",
|
|
2135
|
+
content: `[Orphan ${toolResult.toolName || "tool"} result; call_id=${normalized.callId}]: ${noteText}`,
|
|
2136
|
+
} as ResponseInput[number]);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
if (supportsCustomToolCalls && customCallIds?.has(normalized.callId)) {
|
|
2140
|
+
insertResponsesToolOutput(messages, {
|
|
2141
|
+
type: "custom_tool_call_output",
|
|
2142
|
+
call_id: normalized.callId,
|
|
2143
|
+
output,
|
|
2144
|
+
} as ResponseInput[number]);
|
|
2145
|
+
} else {
|
|
2146
|
+
insertResponsesToolOutput(messages, {
|
|
2147
|
+
type: "function_call_output",
|
|
2148
|
+
call_id: normalized.callId,
|
|
2149
|
+
output,
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (!hasImages || !supportsImages) {
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const contentParts: ResponseInputContent[] = [
|
|
2158
|
+
{ type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText,
|
|
2159
|
+
];
|
|
2160
|
+
for (const block of toolResult.content) {
|
|
2161
|
+
if (block.type === "image") {
|
|
2162
|
+
contentParts.push({
|
|
2163
|
+
type: "input_image",
|
|
2164
|
+
detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
|
|
2165
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
2166
|
+
} satisfies ResponseInputImage);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
const imageMessage = { role: "user", content: contentParts } satisfies ResponseInput[number];
|
|
2170
|
+
syntheticToolImageMessages.add(imageMessage);
|
|
2171
|
+
messages.push(imageMessage);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* Per-block accumulation helpers shared by the two Responses decode loops —
|
|
2176
|
+
* {@link processResponsesStream} (generic Responses) and the Codex stream
|
|
2177
|
+
* handler in `openai-codex-responses.ts`. Each endpoint keeps its own
|
|
2178
|
+
* item-routing, terminal handling, and transport bookkeeping; these own only
|
|
2179
|
+
* the leaf mutations on an already-resolved open block, so the
|
|
2180
|
+
* append/parse/finalize logic lives in exactly one place. The caller passes the
|
|
2181
|
+
* `contentIndex` its router resolved (generic uses `output.content.indexOf`;
|
|
2182
|
+
* Codex uses the open item's recorded index) so the emitted stream events match
|
|
2183
|
+
* each decoder's existing behavior byte-for-byte.
|
|
2184
|
+
*/
|
|
2185
|
+
type ResponsesToolCallBlock = ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number };
|
|
2186
|
+
|
|
2187
|
+
export function appendReasoningSummaryPart(
|
|
2188
|
+
item: ResponseReasoningItem,
|
|
2189
|
+
part: ResponseReasoningItem["summary"][number],
|
|
2190
|
+
): void {
|
|
2191
|
+
item.summary = item.summary || [];
|
|
2192
|
+
item.summary.push(part);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Response-global accumulator for the sequential-cutoff summary contract.
|
|
2197
|
+
*
|
|
2198
|
+
* Summary indices are cumulative across ALL reasoning items in a response:
|
|
2199
|
+
* each new reasoning item replays the previous item's last completed section
|
|
2200
|
+
* (`.done` at index N-1) before streaming its own, and replay-only items may
|
|
2201
|
+
* add nothing new. Folding per item would re-emit every replayed section, so
|
|
2202
|
+
* the canonical summary and the emitted text span items and live here.
|
|
2203
|
+
*/
|
|
2204
|
+
export interface SequentialCutoffSummaryState {
|
|
2205
|
+
/** Latest full text per response-global summary index. */
|
|
2206
|
+
summary: ResponseReasoningItem["summary"];
|
|
2207
|
+
/** Canonical summary text already emitted as thinking deltas across all blocks. */
|
|
2208
|
+
emitted: string;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
export function createSequentialCutoffSummaryState(): SequentialCutoffSummaryState {
|
|
2212
|
+
return { summary: [], emitted: "" };
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Sequential-cutoff streams may repeat the full canonical summary as later parts.
|
|
2216
|
+
function foldReasoningSummary(parts: ResponseReasoningItem["summary"] | undefined): string {
|
|
2217
|
+
if (!parts) return "";
|
|
2218
|
+
let canonical = "";
|
|
2219
|
+
for (const part of parts) {
|
|
2220
|
+
const text = part.text;
|
|
2221
|
+
if (!text || text === canonical) continue;
|
|
2222
|
+
const extendsCanonical = text.startsWith(canonical) && text[canonical.length] === "\n";
|
|
2223
|
+
canonical = !canonical || extendsCanonical ? text : `${canonical}\n\n${text}`;
|
|
2224
|
+
}
|
|
2225
|
+
return canonical;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/** Chooses final reasoning text without making sequential-cutoff results disagree with emitted deltas. */
|
|
2229
|
+
export function finalizeReasoningThinking(
|
|
2230
|
+
item: ResponseReasoningItem,
|
|
2231
|
+
streamedThinking: string,
|
|
2232
|
+
cutoff?: SequentialCutoffSummaryState,
|
|
2233
|
+
): string {
|
|
2234
|
+
if (cutoff) return finalizeCutoffReasoningThinking(item, streamedThinking, cutoff);
|
|
2235
|
+
const summaryThinking = item.summary?.map(part => part.text).join("\n\n") ?? "";
|
|
2236
|
+
if (summaryThinking) return summaryThinking;
|
|
2237
|
+
const contentThinking = item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "";
|
|
2238
|
+
return contentThinking || streamedThinking || "";
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
function finalizeCutoffReasoningThinking(
|
|
2242
|
+
item: ResponseReasoningItem,
|
|
2243
|
+
streamedThinking: string,
|
|
2244
|
+
cutoff: SequentialCutoffSummaryState,
|
|
2245
|
+
): string {
|
|
2246
|
+
// The block's streamed deltas are authoritative: final text must never
|
|
2247
|
+
// disagree with what delta consumers already rendered.
|
|
2248
|
+
if (streamedThinking) return streamedThinking;
|
|
2249
|
+
const summaryThinking = foldReasoningSummary(item.summary);
|
|
2250
|
+
if (summaryThinking) {
|
|
2251
|
+
// The done payload carries the response-cumulative summary. Emit only
|
|
2252
|
+
// what no earlier block already emitted; replay-only items finalize empty.
|
|
2253
|
+
if (cutoff.emitted.startsWith(summaryThinking)) return "";
|
|
2254
|
+
if (!cutoff.emitted || summaryThinking.startsWith(cutoff.emitted)) {
|
|
2255
|
+
const suffix = summaryThinking.slice(cutoff.emitted.length).replace(/^\n+/, "");
|
|
2256
|
+
// Adopt the payload as canonical so later items cannot replay this text.
|
|
2257
|
+
cutoff.summary = item.summary?.map(part => ({ ...part })) ?? [];
|
|
2258
|
+
cutoff.emitted = summaryThinking;
|
|
2259
|
+
return suffix;
|
|
2260
|
+
}
|
|
2261
|
+
// Diverged from streamed text — the deltas already shown win.
|
|
2262
|
+
return "";
|
|
2263
|
+
}
|
|
2264
|
+
return item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "";
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
export function appendReasoningSummaryTextDelta(
|
|
2268
|
+
item: ResponseReasoningItem,
|
|
2269
|
+
block: ThinkingContent,
|
|
2270
|
+
delta: string,
|
|
2271
|
+
stream: AssistantMessageEventStream,
|
|
2272
|
+
output: AssistantMessage,
|
|
2273
|
+
contentIndex: number,
|
|
2274
|
+
): void {
|
|
2275
|
+
item.summary = item.summary || [];
|
|
2276
|
+
const lastPart = item.summary[item.summary.length - 1];
|
|
2277
|
+
if (!lastPart) return;
|
|
2278
|
+
block.thinking += delta;
|
|
2279
|
+
lastPart.text += delta;
|
|
2280
|
+
stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
export function appendReasoningSummaryPartDone(
|
|
2284
|
+
item: ResponseReasoningItem,
|
|
2285
|
+
block: ThinkingContent,
|
|
2286
|
+
stream: AssistantMessageEventStream,
|
|
2287
|
+
output: AssistantMessage,
|
|
2288
|
+
contentIndex: number,
|
|
2289
|
+
): void {
|
|
2290
|
+
item.summary = item.summary || [];
|
|
2291
|
+
const lastPart = item.summary[item.summary.length - 1];
|
|
2292
|
+
if (!lastPart) return;
|
|
2293
|
+
block.thinking += "\n\n";
|
|
2294
|
+
lastPart.text += "\n\n";
|
|
2295
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: "\n\n", partial: output });
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Applies an atomic `response.reasoning_summary_text.done` snapshot.
|
|
2300
|
+
*
|
|
2301
|
+
* Sequential-cutoff summary indices are response-global: later reasoning items
|
|
2302
|
+
* replay earlier sections, resend the accumulated summary as one part, or
|
|
2303
|
+
* complete without new sections. The canonical summary is rebuilt in `state`
|
|
2304
|
+
* (spanning items) and only its append-only suffix is emitted into the current
|
|
2305
|
+
* block. Divergent corrections stay buffered until finalization so delta
|
|
2306
|
+
* consumers never receive suffixes based on unseen replacement text.
|
|
2307
|
+
*/
|
|
2308
|
+
export function applyReasoningSummaryDone(
|
|
2309
|
+
state: SequentialCutoffSummaryState,
|
|
2310
|
+
block: ThinkingContent,
|
|
2311
|
+
text: string,
|
|
2312
|
+
summaryIndex: number,
|
|
2313
|
+
stream: AssistantMessageEventStream,
|
|
2314
|
+
output: AssistantMessage,
|
|
2315
|
+
contentIndex: number,
|
|
2316
|
+
): void {
|
|
2317
|
+
while (state.summary.length <= summaryIndex) {
|
|
2318
|
+
state.summary.push({ type: "summary_text", text: "" });
|
|
2319
|
+
}
|
|
2320
|
+
state.summary[summaryIndex].text = text;
|
|
2321
|
+
const after = foldReasoningSummary(state.summary);
|
|
2322
|
+
if (!after.startsWith(state.emitted)) return;
|
|
2323
|
+
let delta = after.slice(state.emitted.length);
|
|
2324
|
+
if (!delta) return;
|
|
2325
|
+
state.emitted = after;
|
|
2326
|
+
// A fresh block starts a new section: drop the inter-section separator so
|
|
2327
|
+
// each thinking block stands alone.
|
|
2328
|
+
if (!block.thinking) delta = delta.replace(/^\n+/, "");
|
|
2329
|
+
if (!delta) return;
|
|
2330
|
+
block.thinking += delta;
|
|
2331
|
+
stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
export function appendMessageContentPart(
|
|
2335
|
+
item: ResponseOutputMessage,
|
|
2336
|
+
part: ResponseContentPartAddedEvent["part"] | undefined,
|
|
2337
|
+
): void {
|
|
2338
|
+
item.content = item.content || [];
|
|
2339
|
+
if (part && (part.type === "output_text" || part.type === "refusal")) {
|
|
2340
|
+
item.content.push(part);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
export function appendMessageTextDelta(
|
|
2345
|
+
item: ResponseOutputMessage,
|
|
2346
|
+
block: TextContent,
|
|
2347
|
+
delta: string,
|
|
2348
|
+
stream: AssistantMessageEventStream,
|
|
2349
|
+
output: AssistantMessage,
|
|
2350
|
+
contentIndex: number,
|
|
2351
|
+
partType: "output_text" | "refusal",
|
|
2352
|
+
): void {
|
|
2353
|
+
item.content = item.content || [];
|
|
2354
|
+
let lastPart = item.content[item.content.length - 1];
|
|
2355
|
+
if (lastPart?.type !== partType) {
|
|
2356
|
+
// `content_part.added` never arrived (lossy proxy) — synthesize the part
|
|
2357
|
+
// so live text still streams instead of freezing until output_item.done.
|
|
2358
|
+
lastPart =
|
|
2359
|
+
partType === "output_text"
|
|
2360
|
+
? { type: "output_text", text: "", annotations: [] }
|
|
2361
|
+
: { type: "refusal", refusal: "" };
|
|
2362
|
+
item.content.push(lastPart);
|
|
2363
|
+
}
|
|
2364
|
+
block.text += delta;
|
|
2365
|
+
if (lastPart.type === "output_text") {
|
|
2366
|
+
lastPart.text += delta;
|
|
2367
|
+
} else {
|
|
2368
|
+
lastPart.refusal += delta;
|
|
2369
|
+
}
|
|
2370
|
+
stream.push({ type: "text_delta", contentIndex, delta, partial: output });
|
|
2371
|
+
}
|
|
2372
|
+
/** Chooses final message text while treating non-empty terminal content as authoritative. */
|
|
2373
|
+
export function finalizeMessageText(item: ResponseOutputMessage, streamedText: string): string {
|
|
2374
|
+
if (!item.content?.length) return streamedText || "";
|
|
2375
|
+
return item.content.map(part => (part.type === "output_text" ? (part.text ?? "") : (part.refusal ?? ""))).join("");
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
export function accumulateToolCallArgumentsDelta(
|
|
2379
|
+
block: ResponsesToolCallBlock,
|
|
2380
|
+
delta: string,
|
|
2381
|
+
stream: AssistantMessageEventStream,
|
|
2382
|
+
output: AssistantMessage,
|
|
2383
|
+
contentIndex: number,
|
|
2384
|
+
): void {
|
|
2385
|
+
block[kStreamingPartialJson] += delta;
|
|
2386
|
+
const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
|
|
2387
|
+
if (throttled) {
|
|
2388
|
+
block.arguments = throttled.value;
|
|
2389
|
+
block[kStreamingLastParseLen] = throttled.parsedLen;
|
|
2390
|
+
}
|
|
2391
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/**
|
|
2395
|
+
* Finalize streamed function-call arguments from the authoritative `.done`
|
|
2396
|
+
* payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
|
|
2397
|
+
* Codex's block shape has no such field), so this only rewrites `arguments` and
|
|
2398
|
+
* drops the transient accumulation fields.
|
|
2399
|
+
*/
|
|
2400
|
+
export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
|
|
2401
|
+
block[kStreamingPartialJson] = args;
|
|
2402
|
+
block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
|
|
2403
|
+
clearStreamingPartialJson(block);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
export function accumulateCustomToolCallInputDelta(
|
|
2407
|
+
block: ResponsesToolCallBlock,
|
|
2408
|
+
delta: string,
|
|
2409
|
+
stream: AssistantMessageEventStream,
|
|
2410
|
+
output: AssistantMessage,
|
|
2411
|
+
contentIndex: number,
|
|
2412
|
+
): void {
|
|
2413
|
+
block[kStreamingPartialJson] += delta;
|
|
2414
|
+
block.arguments = { input: block[kStreamingPartialJson] };
|
|
2415
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
export function finalizeCustomToolCallInputDone(block: ResponsesToolCallBlock, input: string): void {
|
|
2419
|
+
block.arguments = { input };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
type OpenAIResponsesTerminalStreamEvent =
|
|
2423
|
+
| Extract<ResponseStreamEvent, { type: "response.completed" | "response.incomplete" }>
|
|
2424
|
+
| { type: "response.done"; response?: Partial<OpenAIResponse> };
|
|
2425
|
+
|
|
2426
|
+
function getOpenAIResponsesTerminalEvent(event: ResponseStreamEvent): OpenAIResponsesTerminalStreamEvent | undefined {
|
|
2427
|
+
const type = (event as { type?: unknown }).type;
|
|
2428
|
+
return type === "response.completed" || type === "response.incomplete" || type === "response.done"
|
|
2429
|
+
? (event as OpenAIResponsesTerminalStreamEvent)
|
|
2430
|
+
: undefined;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
export interface ProcessResponsesStreamOptions {
|
|
2434
|
+
onFirstToken?: () => void;
|
|
2435
|
+
onOutputItemDone?: (item: ResponseOutputItem) => void;
|
|
2436
|
+
/**
|
|
2437
|
+
* Called when a terminal `response.completed`, `response.incomplete`, or
|
|
2438
|
+
* `response.done` event is successfully processed. Only invoked on the
|
|
2439
|
+
* successful-completion path; thrown failure (`response.failed`) and
|
|
2440
|
+
* cancellation paths never call this.
|
|
2441
|
+
* Used by callers to detect premature stream closure (i.e. the stream ended
|
|
2442
|
+
* without a recognized terminal event).
|
|
2443
|
+
*/
|
|
2444
|
+
onCompleted?: () => void;
|
|
2445
|
+
/**
|
|
2446
|
+
* Caller-requested service tier, used to bill the served tier when the
|
|
2447
|
+
* response omits the `service_tier` echo. Only applied for `provider: "openai"`.
|
|
2448
|
+
*/
|
|
2449
|
+
requestServiceTier?: ServiceTier;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
export function computerCallMetadata(item: ResponseComputerToolCall): ComputerToolCallMetadata {
|
|
2453
|
+
const actions = item.actions?.length ? item.actions : item.action ? [item.action] : [];
|
|
2454
|
+
return {
|
|
2455
|
+
type: "computer",
|
|
2456
|
+
providerItemId: item.id,
|
|
2457
|
+
actions: structuredCloneJSON(actions) as ComputerAction[],
|
|
2458
|
+
pendingSafetyChecks: structuredCloneJSON(item.pending_safety_checks ?? []),
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/** Append a native Responses image result and emit its completion event. */
|
|
2463
|
+
export function appendResponsesImageResult(
|
|
2464
|
+
output: AssistantMessage,
|
|
2465
|
+
stream: AssistantMessageEventStream,
|
|
2466
|
+
result: string,
|
|
2467
|
+
): void {
|
|
2468
|
+
const image: ImageContent = {
|
|
2469
|
+
type: "image",
|
|
2470
|
+
data: result,
|
|
2471
|
+
mimeType: parseImageMetadata(Buffer.from(result, "base64"))?.mimeType ?? "image/png",
|
|
2472
|
+
};
|
|
2473
|
+
output.content.push(image);
|
|
2474
|
+
stream.push({
|
|
2475
|
+
type: "image_end",
|
|
2476
|
+
contentIndex: output.content.length - 1,
|
|
2477
|
+
content: image,
|
|
2478
|
+
partial: output,
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
export async function processResponsesStream<TApi extends Api>(
|
|
2483
|
+
openaiStream: AsyncIterable<ResponseStreamEvent>,
|
|
2484
|
+
output: AssistantMessage,
|
|
2485
|
+
stream: AssistantMessageEventStream,
|
|
2486
|
+
model: Model<TApi>,
|
|
2487
|
+
options?: ProcessResponsesStreamOptions,
|
|
2488
|
+
): Promise<void> {
|
|
2489
|
+
type StreamingToolCallBlock = ToolCall & {
|
|
2490
|
+
[kStreamingPartialJson]: string;
|
|
2491
|
+
[kStreamingLastParseLen]?: number;
|
|
2492
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
2493
|
+
};
|
|
2494
|
+
interface StreamingItem {
|
|
2495
|
+
item:
|
|
2496
|
+
| ResponseReasoningItem
|
|
2497
|
+
| ResponseOutputMessage
|
|
2498
|
+
| ResponseFunctionToolCall
|
|
2499
|
+
| ResponseCustomToolCall
|
|
2500
|
+
| ResponseComputerToolCall;
|
|
2501
|
+
block: ThinkingContent | TextContent | StreamingToolCallBlock;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// Multiple items (parallel function_calls in particular) can be open at the same
|
|
2505
|
+
// time. OpenAI's spec routes every per-item event by `output_index`/`item_id`;
|
|
2506
|
+
// see https://github.com/can1357/oh-my-pi/issues/1880 — llama.cpp emits parallel
|
|
2507
|
+
// function_call deltas interleaved, and a singleton `current` reference would
|
|
2508
|
+
// fold them into the wrong block and drop arguments on every call but the last.
|
|
2509
|
+
//
|
|
2510
|
+
// OpenAI-compatible hosts can compound this by omitting `item.id` and
|
|
2511
|
+
// `output_index` on `output_item.added` while routing later argument deltas to
|
|
2512
|
+
// either the bare `call_id` or a synthesized `fc_<call_id>` item id. Register
|
|
2513
|
+
// both keys so each delta reaches its own block instead of falling back to the
|
|
2514
|
+
// most recently added parallel call.
|
|
2515
|
+
const openItemsByOutputIndex = new Map<number, StreamingItem>();
|
|
2516
|
+
const openItemsByItemId = new Map<string, StreamingItem>();
|
|
2517
|
+
const openItemsByPrefixedCallId = new Map<string, StreamingItem>();
|
|
2518
|
+
let lastOpenItem: StreamingItem | null = null;
|
|
2519
|
+
const openItemsInOrder: StreamingItem[] = [];
|
|
2520
|
+
|
|
2521
|
+
const prefixedFunctionCallItemKey = (callId: string | undefined): string | undefined =>
|
|
2522
|
+
callId ? `fc_${callId}` : undefined;
|
|
2523
|
+
|
|
2524
|
+
const registerOpenItem = (
|
|
2525
|
+
outputIndex: number | undefined,
|
|
2526
|
+
itemId: string | undefined,
|
|
2527
|
+
entry: StreamingItem,
|
|
2528
|
+
alternateItemKey?: string,
|
|
2529
|
+
prefixedAlternateItemKey?: string,
|
|
2530
|
+
): void => {
|
|
2531
|
+
if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
|
|
2532
|
+
if (itemId) openItemsByItemId.set(itemId, entry);
|
|
2533
|
+
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.set(alternateItemKey, entry);
|
|
2534
|
+
if (
|
|
2535
|
+
prefixedAlternateItemKey &&
|
|
2536
|
+
prefixedAlternateItemKey !== itemId &&
|
|
2537
|
+
prefixedAlternateItemKey !== alternateItemKey
|
|
2538
|
+
) {
|
|
2539
|
+
openItemsByPrefixedCallId.set(prefixedAlternateItemKey, entry);
|
|
2540
|
+
}
|
|
2541
|
+
openItemsInOrder.push(entry);
|
|
2542
|
+
lastOpenItem = entry;
|
|
2543
|
+
};
|
|
2544
|
+
const lookupOpenItem = (event: { output_index?: number; item_id?: string }): StreamingItem | undefined => {
|
|
2545
|
+
const hasKey = typeof event.output_index === "number" || event.item_id !== undefined;
|
|
2546
|
+
if (typeof event.output_index === "number") {
|
|
2547
|
+
const found = openItemsByOutputIndex.get(event.output_index);
|
|
2548
|
+
if (found) return found;
|
|
2549
|
+
}
|
|
2550
|
+
if (event.item_id) {
|
|
2551
|
+
const found = openItemsByItemId.get(event.item_id);
|
|
2552
|
+
if (found) return found;
|
|
2553
|
+
}
|
|
2554
|
+
// Keyed events whose item already closed are stale; drop them instead of
|
|
2555
|
+
// routing to a sibling. Only fully identifierless mock/proxy events use the
|
|
2556
|
+
// legacy singleton fallback.
|
|
2557
|
+
return hasKey ? undefined : (lastOpenItem ?? undefined);
|
|
2558
|
+
};
|
|
2559
|
+
const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
|
|
2560
|
+
typeof event.output_index === "number" || event.item_id !== undefined;
|
|
2561
|
+
const startsJsonObjectDelta = (delta: unknown): boolean => {
|
|
2562
|
+
if (typeof delta !== "string") return false;
|
|
2563
|
+
for (let index = 0; index < delta.length; index++) {
|
|
2564
|
+
const code = delta.charCodeAt(index);
|
|
2565
|
+
if (code === 0x09 || code === 0x0a || code === 0x0d || code === 0x20) continue;
|
|
2566
|
+
return code === 0x7b;
|
|
2567
|
+
}
|
|
2568
|
+
return false;
|
|
2569
|
+
};
|
|
2570
|
+
const shouldAdvanceIdentifierlessFunctionDelta = (
|
|
2571
|
+
event: { output_index?: number; item_id?: string; delta?: unknown },
|
|
2572
|
+
candidate: StreamingItem,
|
|
2573
|
+
): boolean => {
|
|
2574
|
+
const delta = event.delta;
|
|
2575
|
+
if (
|
|
2576
|
+
hasOpenItemKey(event) ||
|
|
2577
|
+
typeof delta !== "string" ||
|
|
2578
|
+
!startsJsonObjectDelta(delta) ||
|
|
2579
|
+
candidate.item.type !== "function_call" ||
|
|
2580
|
+
candidate.block.type !== "toolCall"
|
|
2581
|
+
) {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
const partial = candidate.block[kStreamingPartialJson];
|
|
2585
|
+
if (partial.trim().length === 0) return false;
|
|
2586
|
+
// A `{`-starting identifierless delta is ambiguous: the opening of a new
|
|
2587
|
+
// sibling call, or continuation bytes inside the candidate's own argument
|
|
2588
|
+
// JSON (`{"command":"echo ` + `{1..3}"}`). Advance only when the candidate
|
|
2589
|
+
// cannot absorb the delta: its buffer is already one complete JSON value,
|
|
2590
|
+
// already unsalvageable (lossy hosts abandon buffers mid-string, leaving
|
|
2591
|
+
// raw control characters strict JSON forbids), or the concatenation would
|
|
2592
|
+
// break it. Otherwise the delta is a legal continuation and must stay.
|
|
2593
|
+
const state = classifyJsonPrefix(partial);
|
|
2594
|
+
if (state !== "prefix") return true;
|
|
2595
|
+
return classifyJsonPrefix(partial + delta) === "invalid";
|
|
2596
|
+
};
|
|
2597
|
+
const hasLaterUnfinishedFunctionCall = (start: number): boolean => {
|
|
2598
|
+
for (let index = start + 1; index < openItemsInOrder.length; index++) {
|
|
2599
|
+
const candidate = openItemsInOrder[index];
|
|
2600
|
+
if (
|
|
2601
|
+
candidate?.item.type === "function_call" &&
|
|
2602
|
+
candidate.block.type === "toolCall" &&
|
|
2603
|
+
!candidate.block[kStreamingArgumentsDone]
|
|
2604
|
+
) {
|
|
2605
|
+
return true;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return false;
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
let identifierlessFunctionDeltaTarget: StreamingItem | undefined;
|
|
2612
|
+
|
|
2613
|
+
const lookupOpenToolCallAlias = (
|
|
2614
|
+
event: { output_index?: number; item_id?: string },
|
|
2615
|
+
type: "function_call" | "custom_tool_call",
|
|
2616
|
+
): StreamingItem | undefined => {
|
|
2617
|
+
if (typeof event.output_index === "number") {
|
|
2618
|
+
const byOutputIndex = openItemsByOutputIndex.get(event.output_index);
|
|
2619
|
+
if (byOutputIndex) return byOutputIndex;
|
|
2620
|
+
// A lossy host (llama.cpp/Ollama, issue #2015) can omit `output_index` on
|
|
2621
|
+
// `output_item.added` while still stamping the spec-required field on the
|
|
2622
|
+
// delta. The index was never registered, so fall through to the prefixed
|
|
2623
|
+
// alias / exact item-id maps instead of dropping to `lastOpenItem`.
|
|
2624
|
+
}
|
|
2625
|
+
if (event.item_id) {
|
|
2626
|
+
// Prefixed call-id aliases share the same wire namespace as real call ids.
|
|
2627
|
+
// Argument/input events can use the prefixed form, while final
|
|
2628
|
+
// output_item.done events below use exact call ids; keep aliases in a
|
|
2629
|
+
// separate map so a real `call_id: "fc_x"` cannot overwrite the alias
|
|
2630
|
+
// for `call_id: "x"`.
|
|
2631
|
+
const alias = openItemsByPrefixedCallId.get(event.item_id);
|
|
2632
|
+
if (alias?.item.type === type) return alias;
|
|
2633
|
+
const exact = openItemsByItemId.get(event.item_id);
|
|
2634
|
+
if (exact) return exact;
|
|
2635
|
+
}
|
|
2636
|
+
return lookupOpenItem(event);
|
|
2637
|
+
};
|
|
2638
|
+
const lookupOpenFunctionCallItem = (event: {
|
|
2639
|
+
output_index?: number;
|
|
2640
|
+
item_id?: string;
|
|
2641
|
+
delta?: unknown;
|
|
2642
|
+
}): StreamingItem | undefined => {
|
|
2643
|
+
if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
|
|
2644
|
+
const canContinuePreviousIdentifierlessDelta = typeof event.delta === "string";
|
|
2645
|
+
if (canContinuePreviousIdentifierlessDelta && identifierlessFunctionDeltaTarget) {
|
|
2646
|
+
const targetIndex = openItemsInOrder.indexOf(identifierlessFunctionDeltaTarget);
|
|
2647
|
+
const target = targetIndex >= 0 ? openItemsInOrder[targetIndex] : undefined;
|
|
2648
|
+
if (
|
|
2649
|
+
target?.item.type === "function_call" &&
|
|
2650
|
+
target.block.type === "toolCall" &&
|
|
2651
|
+
!target.block[kStreamingArgumentsDone]
|
|
2652
|
+
) {
|
|
2653
|
+
const shouldAdvanceFromTarget =
|
|
2654
|
+
shouldAdvanceIdentifierlessFunctionDelta(event, target) && hasLaterUnfinishedFunctionCall(targetIndex);
|
|
2655
|
+
if (!shouldAdvanceFromTarget) return target;
|
|
2656
|
+
} else {
|
|
2657
|
+
identifierlessFunctionDeltaTarget = undefined;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
let skippedStartedCandidate = false;
|
|
2661
|
+
for (let index = 0; index < openItemsInOrder.length; index++) {
|
|
2662
|
+
const candidate = openItemsInOrder[index]!;
|
|
2663
|
+
if (
|
|
2664
|
+
candidate.item.type === "function_call" &&
|
|
2665
|
+
candidate.block.type === "toolCall" &&
|
|
2666
|
+
!candidate.block[kStreamingArgumentsDone]
|
|
2667
|
+
) {
|
|
2668
|
+
if (shouldAdvanceIdentifierlessFunctionDelta(event, candidate) && hasLaterUnfinishedFunctionCall(index)) {
|
|
2669
|
+
skippedStartedCandidate = true;
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
if (canContinuePreviousIdentifierlessDelta) identifierlessFunctionDeltaTarget = candidate;
|
|
2673
|
+
return candidate;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
if (skippedStartedCandidate && startsJsonObjectDelta(event.delta)) return undefined;
|
|
2677
|
+
return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
|
|
2678
|
+
};
|
|
2679
|
+
const closeOpenItem = (
|
|
2680
|
+
outputIndex: number | undefined,
|
|
2681
|
+
itemId: string | undefined,
|
|
2682
|
+
entry: StreamingItem | undefined,
|
|
2683
|
+
alternateItemKey?: string,
|
|
2684
|
+
prefixedAlternateItemKey?: string,
|
|
2685
|
+
): void => {
|
|
2686
|
+
if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
|
|
2687
|
+
if (itemId) openItemsByItemId.delete(itemId);
|
|
2688
|
+
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.delete(alternateItemKey);
|
|
2689
|
+
if (
|
|
2690
|
+
prefixedAlternateItemKey &&
|
|
2691
|
+
prefixedAlternateItemKey !== itemId &&
|
|
2692
|
+
prefixedAlternateItemKey !== alternateItemKey &&
|
|
2693
|
+
openItemsByPrefixedCallId.get(prefixedAlternateItemKey) === entry
|
|
2694
|
+
) {
|
|
2695
|
+
openItemsByPrefixedCallId.delete(prefixedAlternateItemKey);
|
|
2696
|
+
}
|
|
2697
|
+
if (entry) {
|
|
2698
|
+
const index = openItemsInOrder.indexOf(entry);
|
|
2699
|
+
if (index >= 0) openItemsInOrder.splice(index, 1);
|
|
2700
|
+
}
|
|
2701
|
+
if (entry && identifierlessFunctionDeltaTarget === entry) identifierlessFunctionDeltaTarget = undefined;
|
|
2702
|
+
if (entry && lastOpenItem === entry) lastOpenItem = null;
|
|
2703
|
+
};
|
|
2704
|
+
const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
|
|
2705
|
+
output.content.indexOf(block);
|
|
2706
|
+
|
|
2707
|
+
let sawFirstToken = false;
|
|
2708
|
+
|
|
2709
|
+
for await (const event of openaiStream) {
|
|
2710
|
+
const terminalEvent = getOpenAIResponsesTerminalEvent(event);
|
|
2711
|
+
if (event.type === "response.created") {
|
|
2712
|
+
output.responseId = event.response.id;
|
|
2713
|
+
} else if (event.type === "response.output_item.added") {
|
|
2714
|
+
if (!sawFirstToken) {
|
|
2715
|
+
sawFirstToken = true;
|
|
2716
|
+
options?.onFirstToken?.();
|
|
2717
|
+
}
|
|
2718
|
+
const item = event.item;
|
|
2719
|
+
if (item.type === "reasoning") {
|
|
2720
|
+
const block: ThinkingContent = { type: "thinking", thinking: "", itemId: item.id };
|
|
2721
|
+
output.content.push(block);
|
|
2722
|
+
registerOpenItem(event.output_index, item.id, { item, block });
|
|
2723
|
+
stream.push({ type: "thinking_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2724
|
+
} else if (item.type === "message") {
|
|
2725
|
+
const block: TextContent = {
|
|
2726
|
+
type: "text",
|
|
2727
|
+
text: "",
|
|
2728
|
+
textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined),
|
|
2729
|
+
};
|
|
2730
|
+
output.content.push(block);
|
|
2731
|
+
registerOpenItem(event.output_index, item.id, { item, block });
|
|
2732
|
+
stream.push({ type: "text_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2733
|
+
} else if (item.type === "function_call") {
|
|
2734
|
+
const block: StreamingToolCallBlock = {
|
|
2735
|
+
type: "toolCall",
|
|
2736
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2737
|
+
name: item.name,
|
|
2738
|
+
arguments: {},
|
|
2739
|
+
[kStreamingPartialJson]: item.arguments || "",
|
|
2740
|
+
};
|
|
2741
|
+
output.content.push(block);
|
|
2742
|
+
registerOpenItem(
|
|
2743
|
+
event.output_index,
|
|
2744
|
+
item.id,
|
|
2745
|
+
{ item, block },
|
|
2746
|
+
item.call_id,
|
|
2747
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
2748
|
+
);
|
|
2749
|
+
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2750
|
+
} else if (item.type === "computer_call") {
|
|
2751
|
+
const block: StreamingToolCallBlock = {
|
|
2752
|
+
type: "toolCall",
|
|
2753
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2754
|
+
name: "computer",
|
|
2755
|
+
arguments: {},
|
|
2756
|
+
providerMetadata: computerCallMetadata(item),
|
|
2757
|
+
[kStreamingPartialJson]: "",
|
|
2758
|
+
};
|
|
2759
|
+
output.content.push(block);
|
|
2760
|
+
registerOpenItem(event.output_index, item.id, { item, block }, item.call_id);
|
|
2761
|
+
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2762
|
+
} else if (item.type === "custom_tool_call") {
|
|
2763
|
+
const block: StreamingToolCallBlock = {
|
|
2764
|
+
type: "toolCall",
|
|
2765
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2766
|
+
// Preserve the raw wire name (e.g. `apply_patch`). The agent-loop
|
|
2767
|
+
// dispatcher matches it against both `Tool.name` and
|
|
2768
|
+
// `Tool.customWireName`, so this stays wire-accurate through
|
|
2769
|
+
// history replay while still routing to the right handler.
|
|
2770
|
+
name: item.name,
|
|
2771
|
+
arguments: { input: item.input ?? "" },
|
|
2772
|
+
customWireName: item.name,
|
|
2773
|
+
// Custom tools stream a raw string, but we reuse `partialJson` as the
|
|
2774
|
+
// accumulation buffer so later code that inspects the field still works.
|
|
2775
|
+
[kStreamingPartialJson]: item.input ?? "",
|
|
2776
|
+
};
|
|
2777
|
+
output.content.push(block);
|
|
2778
|
+
registerOpenItem(
|
|
2779
|
+
event.output_index,
|
|
2780
|
+
item.id,
|
|
2781
|
+
{ item, block },
|
|
2782
|
+
item.call_id,
|
|
2783
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
2784
|
+
);
|
|
2785
|
+
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2786
|
+
}
|
|
2787
|
+
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
2788
|
+
const entry = lookupOpenItem(event);
|
|
2789
|
+
if (entry?.item.type === "reasoning") appendReasoningSummaryPart(entry.item, event.part);
|
|
2790
|
+
} else if (event.type === "response.reasoning_summary_text.delta") {
|
|
2791
|
+
const entry = lookupOpenItem(event);
|
|
2792
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2793
|
+
appendReasoningSummaryTextDelta(
|
|
2794
|
+
entry.item,
|
|
2795
|
+
entry.block,
|
|
2796
|
+
event.delta,
|
|
2797
|
+
stream,
|
|
2798
|
+
output,
|
|
2799
|
+
contentIndexOf(entry.block),
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
} else if (event.type === "response.reasoning_summary_part.done") {
|
|
2803
|
+
const entry = lookupOpenItem(event);
|
|
2804
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2805
|
+
appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, contentIndexOf(entry.block));
|
|
2806
|
+
}
|
|
2807
|
+
} else if (event.type === "response.reasoning_text.delta") {
|
|
2808
|
+
// Raw reasoning text delta from local providers that stream thinking
|
|
2809
|
+
// directly rather than via the OpenAI summary tracking protocol.
|
|
2810
|
+
const entry = lookupOpenItem(event);
|
|
2811
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2812
|
+
entry.block.thinking += event.delta;
|
|
2813
|
+
stream.push({
|
|
2814
|
+
type: "thinking_delta",
|
|
2815
|
+
contentIndex: contentIndexOf(entry.block),
|
|
2816
|
+
delta: event.delta,
|
|
2817
|
+
partial: output,
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
} else if (event.type === "response.content_part.added") {
|
|
2821
|
+
const entry = lookupOpenItem(event);
|
|
2822
|
+
if (entry?.item.type === "message") appendMessageContentPart(entry.item, event.part);
|
|
2823
|
+
} else if (event.type === "response.output_text.delta") {
|
|
2824
|
+
const entry = lookupOpenItem(event);
|
|
2825
|
+
if (entry?.item.type === "message" && entry.block.type === "text") {
|
|
2826
|
+
appendMessageTextDelta(
|
|
2827
|
+
entry.item,
|
|
2828
|
+
entry.block,
|
|
2829
|
+
event.delta,
|
|
2830
|
+
stream,
|
|
2831
|
+
output,
|
|
2832
|
+
contentIndexOf(entry.block),
|
|
2833
|
+
"output_text",
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
} else if (event.type === "response.refusal.delta") {
|
|
2837
|
+
const entry = lookupOpenItem(event);
|
|
2838
|
+
if (entry?.item.type === "message" && entry.block.type === "text") {
|
|
2839
|
+
appendMessageTextDelta(
|
|
2840
|
+
entry.item,
|
|
2841
|
+
entry.block,
|
|
2842
|
+
event.delta,
|
|
2843
|
+
stream,
|
|
2844
|
+
output,
|
|
2845
|
+
contentIndexOf(entry.block),
|
|
2846
|
+
"refusal",
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
} else if (event.type === "response.function_call_arguments.delta") {
|
|
2850
|
+
const entry = lookupOpenFunctionCallItem(event);
|
|
2851
|
+
if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
|
|
2852
|
+
accumulateToolCallArgumentsDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
|
|
2853
|
+
}
|
|
2854
|
+
} else if (event.type === "response.function_call_arguments.done") {
|
|
2855
|
+
const entry = lookupOpenFunctionCallItem(event);
|
|
2856
|
+
if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
|
|
2857
|
+
finalizeToolCallArgumentsDone(entry.block, event.arguments);
|
|
2858
|
+
entry.block[kStreamingArgumentsDone] = true;
|
|
2859
|
+
}
|
|
2860
|
+
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
2861
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
2862
|
+
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
2863
|
+
accumulateCustomToolCallInputDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
|
|
2864
|
+
}
|
|
2865
|
+
} else if (event.type === "response.custom_tool_call_input.done") {
|
|
2866
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
2867
|
+
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
2868
|
+
finalizeCustomToolCallInputDone(entry.block, event.input);
|
|
2869
|
+
entry.block[kStreamingArgumentsDone] = true;
|
|
2870
|
+
}
|
|
2871
|
+
} else if (event.type === "response.output_item.done") {
|
|
2872
|
+
const item = structuredCloneJSON(event.item);
|
|
2873
|
+
options?.onOutputItemDone?.(item);
|
|
2874
|
+
const entry =
|
|
2875
|
+
item.type === "function_call" || item.type === "custom_tool_call"
|
|
2876
|
+
? lookupOpenItem({ output_index: event.output_index, item_id: item.id ?? item.call_id })
|
|
2877
|
+
: lookupOpenItem({ output_index: event.output_index, item_id: item.id });
|
|
2878
|
+
if (item.type === "reasoning") {
|
|
2879
|
+
// Prefer the routed entry; the bare itemId find misroutes when ids are
|
|
2880
|
+
// absent (`undefined === undefined` matches the FIRST thinking block) and
|
|
2881
|
+
// misses entirely when the done-event id drifts from the added-event id.
|
|
2882
|
+
const reasoningBlock =
|
|
2883
|
+
entry?.block.type === "thinking"
|
|
2884
|
+
? entry.block
|
|
2885
|
+
: (output.content.find(b => b.type === "thinking" && (b as ThinkingContent).itemId === item.id) as
|
|
2886
|
+
| ThinkingContent
|
|
2887
|
+
| undefined);
|
|
2888
|
+
if (reasoningBlock) {
|
|
2889
|
+
reasoningBlock.thinking = finalizeReasoningThinking(item, reasoningBlock.thinking);
|
|
2890
|
+
reasoningBlock.thinkingSignature = JSON.stringify(item);
|
|
2891
|
+
stream.push({
|
|
2892
|
+
type: "thinking_end",
|
|
2893
|
+
contentIndex: contentIndexOf(reasoningBlock),
|
|
2894
|
+
content: reasoningBlock.thinking,
|
|
2895
|
+
partial: output,
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
closeOpenItem(event.output_index, item.id, entry);
|
|
2899
|
+
} else if (item.type === "message") {
|
|
2900
|
+
const block = entry?.block.type === "text" ? entry.block : undefined;
|
|
2901
|
+
const text = finalizeMessageText(item, block?.text ?? "");
|
|
2902
|
+
const textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
|
|
2903
|
+
let contentIndex: number;
|
|
2904
|
+
if (block) {
|
|
2905
|
+
block.text = text;
|
|
2906
|
+
block.textSignature = textSignature;
|
|
2907
|
+
contentIndex = contentIndexOf(block);
|
|
2908
|
+
} else {
|
|
2909
|
+
// `output_item.added` never arrived (lossy proxy) — synthesize the
|
|
2910
|
+
// block so the final message still carries the authoritative text.
|
|
2911
|
+
const synthesized: TextContent = { type: "text", text, textSignature };
|
|
2912
|
+
output.content.push(synthesized);
|
|
2913
|
+
contentIndex = output.content.length - 1;
|
|
2914
|
+
}
|
|
2915
|
+
stream.push({ type: "text_end", contentIndex, content: text, partial: output });
|
|
2916
|
+
closeOpenItem(event.output_index, item.id, entry);
|
|
2917
|
+
} else if (item.type === "function_call") {
|
|
2918
|
+
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2919
|
+
const args = block?.[kStreamingArgumentsDone]
|
|
2920
|
+
? block.arguments
|
|
2921
|
+
: item.arguments
|
|
2922
|
+
? parseStreamingJson(item.arguments)
|
|
2923
|
+
: block?.[kStreamingPartialJson]
|
|
2924
|
+
? parseStreamingJson(block[kStreamingPartialJson])
|
|
2925
|
+
: parseStreamingJson("{}");
|
|
2926
|
+
const toolCall: ToolCall = {
|
|
2927
|
+
type: "toolCall",
|
|
2928
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2929
|
+
name: item.name,
|
|
2930
|
+
arguments: args,
|
|
2931
|
+
};
|
|
2932
|
+
let contentIndex: number;
|
|
2933
|
+
if (block) {
|
|
2934
|
+
// Persist the authoritative final args on the stored block. The
|
|
2935
|
+
// throttled delta parser may have skipped the last partial parse,
|
|
2936
|
+
// leaving block.arguments stale (often `{}`); the emitted toolCall
|
|
2937
|
+
// and the persisted block must agree.
|
|
2938
|
+
block.arguments = args;
|
|
2939
|
+
clearStreamingPartialJson(block);
|
|
2940
|
+
contentIndex = contentIndexOf(block);
|
|
2941
|
+
} else {
|
|
2942
|
+
// `output_item.added` never arrived (lossy proxy) — synthesize the
|
|
2943
|
+
// block so the final message carries the call the consumer was told
|
|
2944
|
+
// completed (the agent loop executes tools from message.content).
|
|
2945
|
+
output.content.push(toolCall);
|
|
2946
|
+
contentIndex = output.content.length - 1;
|
|
2947
|
+
}
|
|
2948
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2949
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2950
|
+
} else if (item.type === "computer_call") {
|
|
2951
|
+
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2952
|
+
const toolCall: ToolCall = {
|
|
2953
|
+
type: "toolCall",
|
|
2954
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2955
|
+
name: "computer",
|
|
2956
|
+
arguments: {},
|
|
2957
|
+
providerMetadata: computerCallMetadata(item),
|
|
2958
|
+
};
|
|
2959
|
+
let contentIndex: number;
|
|
2960
|
+
if (block) {
|
|
2961
|
+
block.id = toolCall.id;
|
|
2962
|
+
block.providerMetadata = toolCall.providerMetadata;
|
|
2963
|
+
clearStreamingPartialJson(block);
|
|
2964
|
+
contentIndex = contentIndexOf(block);
|
|
2965
|
+
} else {
|
|
2966
|
+
output.content.push(toolCall);
|
|
2967
|
+
contentIndex = output.content.length - 1;
|
|
2968
|
+
}
|
|
2969
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id);
|
|
2970
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2971
|
+
} else if (item.type === "custom_tool_call") {
|
|
2972
|
+
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2973
|
+
const rawInput = block?.[kStreamingPartialJson] ? block[kStreamingPartialJson] : (item.input ?? "");
|
|
2974
|
+
const toolCall: ToolCall = {
|
|
2975
|
+
type: "toolCall",
|
|
2976
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2977
|
+
name: item.name,
|
|
2978
|
+
arguments: { input: rawInput },
|
|
2979
|
+
customWireName: item.name,
|
|
2980
|
+
};
|
|
2981
|
+
let contentIndex: number;
|
|
2982
|
+
if (block) {
|
|
2983
|
+
// Persist the final input on the stored block and drop the transient
|
|
2984
|
+
// accumulation buffer, mirroring the function_call branch above.
|
|
2985
|
+
block.arguments = { input: rawInput };
|
|
2986
|
+
clearStreamingPartialJson(block);
|
|
2987
|
+
contentIndex = contentIndexOf(block);
|
|
2988
|
+
} else {
|
|
2989
|
+
output.content.push(toolCall);
|
|
2990
|
+
contentIndex = output.content.length - 1;
|
|
2991
|
+
}
|
|
2992
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2993
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2994
|
+
} else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
|
|
2995
|
+
appendResponsesImageResult(output, stream, item.result);
|
|
2996
|
+
}
|
|
2997
|
+
} else if (terminalEvent) {
|
|
2998
|
+
const response = terminalEvent.response;
|
|
2999
|
+
const shouldPromoteIncompleteToolUse =
|
|
3000
|
+
response?.status === "incomplete" &&
|
|
3001
|
+
response.incomplete_details?.reason === "max_output_tokens" &&
|
|
3002
|
+
hasExecutableIncompleteResponsesToolCalls(output);
|
|
3003
|
+
finalizePendingResponsesToolCalls(output);
|
|
3004
|
+
if (response?.id) {
|
|
3005
|
+
output.responseId = response.id;
|
|
3006
|
+
}
|
|
3007
|
+
populateResponsesUsageFromResponse(output, response?.usage);
|
|
3008
|
+
calculateCost(model, output.usage);
|
|
3009
|
+
applyOpenRouterReportedCost(model, output.usage, response?.usage);
|
|
3010
|
+
applyOpenAIResponsesServiceTierCost(
|
|
3011
|
+
model,
|
|
3012
|
+
output.usage,
|
|
3013
|
+
(response as { service_tier?: unknown } | undefined)?.service_tier,
|
|
3014
|
+
options?.requestServiceTier,
|
|
3015
|
+
);
|
|
3016
|
+
output.stopReason = mapOpenAIResponsesStopReason(response?.status);
|
|
3017
|
+
if (response?.status === "failed" || response?.status === "cancelled") {
|
|
3018
|
+
const error = response?.error ?? (response as any)?.status_details?.error;
|
|
3019
|
+
const details = response?.incomplete_details;
|
|
3020
|
+
const statusDetailsReason = (response as any)?.status_details?.reason;
|
|
3021
|
+
const message = error
|
|
3022
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
3023
|
+
: details?.reason
|
|
3024
|
+
? `incomplete: ${details.reason}`
|
|
3025
|
+
: typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
|
|
3026
|
+
? `status_details: ${statusDetailsReason}`
|
|
3027
|
+
: "Unknown error (no error details in response)";
|
|
3028
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
3029
|
+
}
|
|
3030
|
+
if (response?.status === "incomplete" && response.incomplete_details?.reason === "content_filter") {
|
|
3031
|
+
// A content-filtered turn is a failure, not a token-cap truncation —
|
|
3032
|
+
// mapping it to "length" would route the agent loop into "shorten your
|
|
3033
|
+
// output" recovery against a filtered prompt.
|
|
3034
|
+
throw new AIError.ProviderResponseError("incomplete: content_filter", {
|
|
3035
|
+
provider: model.provider,
|
|
3036
|
+
kind: "content-blocked",
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
promoteResponsesToolUseStopReason(
|
|
3040
|
+
output,
|
|
3041
|
+
(response as { end_turn?: boolean } | undefined)?.end_turn,
|
|
3042
|
+
shouldPromoteIncompleteToolUse,
|
|
3043
|
+
);
|
|
3044
|
+
options?.onCompleted?.();
|
|
3045
|
+
// `response.completed`/`response.incomplete`/`response.done` is the last event of a
|
|
3046
|
+
// Responses stream. Stop pulling instead of waiting for the server to
|
|
3047
|
+
// close the connection: misbehaving providers keep the socket open
|
|
3048
|
+
// after the terminal event, which would park this loop until the idle
|
|
3049
|
+
// watchdog converts an already-successful turn into a timeout error.
|
|
3050
|
+
// Breaking unwinds the iterator chain (the consumer's `.return()`
|
|
3051
|
+
// reaches the SDK stream), actively releasing the connection.
|
|
3052
|
+
break;
|
|
3053
|
+
} else if (event.type === "error") {
|
|
3054
|
+
const err = (event as any).error ?? event;
|
|
3055
|
+
const code = err.code ?? "unknown";
|
|
3056
|
+
const message = err.message ?? "no message";
|
|
3057
|
+
throw new AIError.ProviderResponseError(`Error Code ${code}: ${message}`, {
|
|
3058
|
+
provider: model.provider,
|
|
3059
|
+
kind: "output",
|
|
3060
|
+
});
|
|
3061
|
+
} else if (event.type === "response.failed") {
|
|
3062
|
+
populateResponsesUsageFromResponse(output, event.response?.usage);
|
|
3063
|
+
const error = event.response?.error ?? (event.response as any)?.status_details?.error;
|
|
3064
|
+
const details = event.response?.incomplete_details;
|
|
3065
|
+
const message = error
|
|
3066
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
3067
|
+
: details?.reason
|
|
3068
|
+
? `incomplete: ${details.reason}`
|
|
3069
|
+
: "Unknown error (no error details in response)";
|
|
3070
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
export function mapOpenAIResponsesStopReason(status: ResponseStatus | undefined): StopReason {
|
|
3076
|
+
if (!status) return "stop";
|
|
3077
|
+
switch (status) {
|
|
3078
|
+
case "completed":
|
|
3079
|
+
return "stop";
|
|
3080
|
+
case "incomplete":
|
|
3081
|
+
return "length";
|
|
3082
|
+
case "failed":
|
|
3083
|
+
case "cancelled":
|
|
3084
|
+
return "error";
|
|
3085
|
+
case "in_progress":
|
|
3086
|
+
case "queued":
|
|
3087
|
+
return "stop";
|
|
3088
|
+
default: {
|
|
3089
|
+
// Compile-time exhaustiveness; at runtime a brand-new status from the
|
|
3090
|
+
// server must degrade gracefully instead of failing a fully-streamed
|
|
3091
|
+
// response.
|
|
3092
|
+
const exhaustive: never = status;
|
|
3093
|
+
logger.warn("Unhandled OpenAI Responses stop reason", { status: exhaustive });
|
|
3094
|
+
return "stop";
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
export function hasExecutableIncompleteResponsesToolCalls(output: AssistantMessage): boolean {
|
|
3100
|
+
let hasToolCall = false;
|
|
3101
|
+
for (const block of output.content) {
|
|
3102
|
+
if (block.type !== "toolCall") continue;
|
|
3103
|
+
hasToolCall = true;
|
|
3104
|
+
const pending = block as ToolCall & {
|
|
3105
|
+
[kStreamingPartialJson]?: string;
|
|
3106
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
3107
|
+
};
|
|
3108
|
+
if (pending.providerMetadata?.type === "computer") {
|
|
3109
|
+
if (pending.providerMetadata.actions.length === 0) return false;
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
const rawArguments = pending[kStreamingPartialJson];
|
|
3113
|
+
// `output_item.done` is not positive completion proof: our Responses
|
|
3114
|
+
// compatibility encoder force-closes still-open calls before forwarding an
|
|
3115
|
+
// upstream `length` stop. Only an explicit arguments/input-done event sets
|
|
3116
|
+
// this marker; an open ordinary call can instead prove completion with its
|
|
3117
|
+
// retained strict-complete JSON.
|
|
3118
|
+
if (pending[kStreamingArgumentsDone]) continue;
|
|
3119
|
+
if (pending.customWireName !== undefined || rawArguments === undefined) return false;
|
|
3120
|
+
if (classifyJsonPrefix(rawArguments) !== "complete") return false;
|
|
3121
|
+
}
|
|
3122
|
+
return hasToolCall;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/**
|
|
3126
|
+
* Finalize any streamed toolCall block whose `output_item.done` never arrived
|
|
3127
|
+
* (lossy proxy, or a terminal event that raced the per-item done): parse the
|
|
3128
|
+
* accumulated `partialJson` into authoritative arguments and strip the transient
|
|
3129
|
+
* streaming fields so they never persist. Shared by the chat-Responses decoder
|
|
3130
|
+
* and the Codex decoder. Closed blocks already cleared these fields, so walking
|
|
3131
|
+
* the full content list leaves them untouched.
|
|
3132
|
+
*/
|
|
3133
|
+
export function finalizePendingResponsesToolCalls(output: AssistantMessage): void {
|
|
3134
|
+
for (const block of output.content) {
|
|
3135
|
+
if (block.type !== "toolCall") continue;
|
|
3136
|
+
const pending = block as ToolCall & {
|
|
3137
|
+
[kStreamingPartialJson]?: string;
|
|
3138
|
+
[kStreamingLastParseLen]?: number;
|
|
3139
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
3140
|
+
};
|
|
3141
|
+
if (pending[kStreamingPartialJson] && !pending[kStreamingArgumentsDone]) {
|
|
3142
|
+
pending.arguments =
|
|
3143
|
+
pending.customWireName !== undefined
|
|
3144
|
+
? { input: pending[kStreamingPartialJson] }
|
|
3145
|
+
: parseStreamingJson(pending[kStreamingPartialJson]);
|
|
3146
|
+
}
|
|
3147
|
+
clearStreamingPartialJson(pending);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
/**
|
|
3152
|
+
* Apply the Responses terminal stop-reason invariants shared by the chat-Responses
|
|
3153
|
+
* and Codex decoders: a turn that produced tool calls becomes `toolUse`, and a
|
|
3154
|
+
* Codex-lineage `end_turn: false` marker pauses the turn so the agent loop
|
|
3155
|
+
* re-samples instead of ending. Callers set `output.stopReason` from the wire
|
|
3156
|
+
* status first via {@link mapOpenAIResponsesStopReason}.
|
|
3157
|
+
*/
|
|
3158
|
+
export function promoteResponsesToolUseStopReason(
|
|
3159
|
+
output: AssistantMessage,
|
|
3160
|
+
endTurn: boolean | undefined,
|
|
3161
|
+
promoteIncompleteToolUse = false,
|
|
3162
|
+
): void {
|
|
3163
|
+
if (
|
|
3164
|
+
output.content.some(block => block.type === "toolCall") &&
|
|
3165
|
+
(output.stopReason === "stop" || (promoteIncompleteToolUse && output.stopReason === "length"))
|
|
3166
|
+
) {
|
|
3167
|
+
output.stopReason = "toolUse";
|
|
3168
|
+
}
|
|
3169
|
+
if (endTurn === false && output.stopReason === "stop") {
|
|
3170
|
+
output.stopDetails = { type: "pause_turn" };
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/** Initial empty `AssistantMessage` that streaming providers accumulate into. */
|
|
3175
|
+
export function createInitialResponsesAssistantMessage(api: Api, provider: string, modelId: string): AssistantMessage {
|
|
3176
|
+
return {
|
|
3177
|
+
role: "assistant",
|
|
3178
|
+
content: [],
|
|
3179
|
+
api,
|
|
3180
|
+
provider,
|
|
3181
|
+
model: modelId,
|
|
3182
|
+
usage: {
|
|
3183
|
+
input: 0,
|
|
3184
|
+
output: 0,
|
|
3185
|
+
cacheRead: 0,
|
|
3186
|
+
cacheWrite: 0,
|
|
3187
|
+
totalTokens: 0,
|
|
3188
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
3189
|
+
},
|
|
3190
|
+
stopReason: "stop",
|
|
3191
|
+
timestamp: Date.now(),
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
/** Extension fields we add on top of `ResponseCreateParamsStreaming` across the Responses-family providers. */
|
|
3196
|
+
export type ResponsesSamplingParamsExtras = {
|
|
3197
|
+
top_p?: number;
|
|
3198
|
+
top_k?: number;
|
|
3199
|
+
min_p?: number;
|
|
3200
|
+
presence_penalty?: number;
|
|
3201
|
+
repetition_penalty?: number;
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
type CommonResponsesParams = ResponseCreateParamsStreaming & ResponsesSamplingParamsExtras;
|
|
3205
|
+
|
|
3206
|
+
type CommonSamplingOptions = Pick<
|
|
3207
|
+
StreamOptions,
|
|
3208
|
+
"temperature" | "topP" | "topK" | "minP" | "presencePenalty" | "repetitionPenalty" | "maxTokens"
|
|
3209
|
+
> & { serviceTier?: ServiceTier };
|
|
3210
|
+
|
|
3211
|
+
/**
|
|
3212
|
+
* Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
|
|
3213
|
+
* temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
|
|
3214
|
+
*
|
|
3215
|
+
* `max_output_tokens` is suppressed when {@link Model.omitMaxOutputTokens} is `true`, so
|
|
3216
|
+
* proxies (notably Ollama) that forward to upstream APIs with an unknown output-token cap
|
|
3217
|
+
* can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
|
|
3218
|
+
* reflect the model's context window rather than the upstream output limit.
|
|
3219
|
+
*/
|
|
3220
|
+
export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
|
|
3221
|
+
params: P,
|
|
3222
|
+
options: CommonSamplingOptions | undefined,
|
|
3223
|
+
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
|
|
3224
|
+
compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
|
|
3225
|
+
},
|
|
3226
|
+
): void {
|
|
3227
|
+
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
|
3228
|
+
params.max_output_tokens = Math.min(
|
|
3229
|
+
options.maxTokens,
|
|
3230
|
+
model.maxTokens ?? Number.POSITIVE_INFINITY,
|
|
3231
|
+
resolveOpenAIResponsesOutputClamp(model) ?? OPENAI_MAX_OUTPUT_TOKENS,
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
|
3235
|
+
// sampling params with a 400 on every serving host (#5606).
|
|
3236
|
+
if (model.compat.supportsSamplingParams) {
|
|
3237
|
+
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
3238
|
+
if (options?.topP !== undefined) params.top_p = options.topP;
|
|
3239
|
+
if (options?.topK !== undefined) params.top_k = options.topK;
|
|
3240
|
+
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
3241
|
+
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
3242
|
+
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
3243
|
+
}
|
|
3244
|
+
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
type ReasoningOptions = {
|
|
3248
|
+
reasoning?: string;
|
|
3249
|
+
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
3250
|
+
disableReasoning?: boolean;
|
|
3251
|
+
toolChoice?: unknown;
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
export interface ApplyResponsesCompatPolicyOptions {
|
|
3255
|
+
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
3256
|
+
mapEffort?: (effort: string) => string;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(
|
|
3260
|
+
params: P,
|
|
3261
|
+
policy: OpenAICompatPolicy,
|
|
3262
|
+
options: ApplyResponsesCompatPolicyOptions | undefined,
|
|
3263
|
+
): void {
|
|
3264
|
+
const reasoning = policy.reasoning;
|
|
3265
|
+
if (!reasoning.modelSupported) return;
|
|
3266
|
+
if (reasoning.includeEncryptedReasoning) {
|
|
3267
|
+
const include = params.include ?? [];
|
|
3268
|
+
if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
|
|
3269
|
+
params.include = include;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
if (reasoning.disabled) {
|
|
3273
|
+
if (reasoning.disableMode === "openrouter-enabled-false") {
|
|
3274
|
+
params.reasoning = { enabled: false } as P["reasoning"];
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
if (
|
|
3278
|
+
reasoning.disableMode === "lowest-effort" &&
|
|
3279
|
+
reasoning.wireEffort !== undefined &&
|
|
3280
|
+
!reasoning.omitReasoningEffort
|
|
3281
|
+
) {
|
|
3282
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
3283
|
+
params.reasoning = { effort: reasoning.wireEffort as ReasoningParam["effort"] } as P["reasoning"] &
|
|
3284
|
+
ReasoningParam;
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
if (reasoning.requestedEffort !== undefined || options?.reasoningSummary !== undefined) {
|
|
3291
|
+
if (reasoning.omitReasoningEffort) {
|
|
3292
|
+
if (options?.reasoningSummary !== undefined && options.reasoningSummary !== null) {
|
|
3293
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
3294
|
+
params.reasoning = { summary: options.reasoningSummary || "auto" } as P["reasoning"] & ReasoningParam;
|
|
3295
|
+
}
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
const requested = reasoning.requestedEffort ?? "medium";
|
|
3300
|
+
const wireEffort = reasoning.wireEffort ?? options?.mapEffort?.(requested) ?? requested;
|
|
3301
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
3302
|
+
const reasoningParams: ReasoningParam = {
|
|
3303
|
+
effort: wireEffort as ReasoningParam["effort"],
|
|
3304
|
+
};
|
|
3305
|
+
if (options?.reasoningSummary !== null) {
|
|
3306
|
+
reasoningParams.summary = options?.reasoningSummary || "auto";
|
|
3307
|
+
}
|
|
3308
|
+
params.reasoning = reasoningParams as P["reasoning"];
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
/**
|
|
3314
|
+
* Apply reasoning-related Responses parameters. Default behavior comes from
|
|
3315
|
+
* catalog compat; include/omit arguments are explicit adapter-wrapper overrides.
|
|
3316
|
+
*/
|
|
3317
|
+
export function applyResponsesReasoningParams<P extends ResponseCreateParamsStreaming>(
|
|
3318
|
+
params: P,
|
|
3319
|
+
model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
|
|
3320
|
+
options: ReasoningOptions | undefined,
|
|
3321
|
+
mapEffort?: (effort: string) => string,
|
|
3322
|
+
includeEncryptedReasoning?: boolean,
|
|
3323
|
+
omitReasoningEffort?: boolean,
|
|
3324
|
+
): void {
|
|
3325
|
+
return applyResponsesCompatPolicy(
|
|
3326
|
+
params,
|
|
3327
|
+
resolveOpenAICompatPolicy(model, {
|
|
3328
|
+
endpoint: "responses",
|
|
3329
|
+
reasoning: options?.reasoning,
|
|
3330
|
+
disableReasoning: options?.disableReasoning,
|
|
3331
|
+
toolChoice: options?.toolChoice,
|
|
3332
|
+
includeEncryptedReasoning,
|
|
3333
|
+
omitReasoningEffort,
|
|
3334
|
+
}),
|
|
3335
|
+
{ reasoningSummary: options?.reasoningSummary, mapEffort },
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
/** Populate `output.usage` from a Responses-API `response.usage` payload. Does not invoke `calculateCost`. */
|
|
3340
|
+
export function populateResponsesUsageFromResponse(
|
|
3341
|
+
output: AssistantMessage,
|
|
3342
|
+
usage:
|
|
3343
|
+
| {
|
|
3344
|
+
input_tokens?: number | null;
|
|
3345
|
+
output_tokens?: number | null;
|
|
3346
|
+
total_tokens?: number | null;
|
|
3347
|
+
prompt_cache_hit_tokens?: number | null;
|
|
3348
|
+
prompt_cache_miss_tokens?: number | null;
|
|
3349
|
+
input_tokens_details?: {
|
|
3350
|
+
cached_tokens?: number | null;
|
|
3351
|
+
cache_write_tokens?: number | null;
|
|
3352
|
+
orchestration_input_tokens?: number | null;
|
|
3353
|
+
orchestration_input_cached_tokens?: number | null;
|
|
3354
|
+
} | null;
|
|
3355
|
+
output_tokens_details?: {
|
|
3356
|
+
reasoning_tokens?: number | null;
|
|
3357
|
+
orchestration_output_tokens?: number | null;
|
|
3358
|
+
} | null;
|
|
3359
|
+
}
|
|
3360
|
+
| null
|
|
3361
|
+
| undefined,
|
|
3362
|
+
): void {
|
|
3363
|
+
if (!usage) return;
|
|
3364
|
+
const details = usage.input_tokens_details;
|
|
3365
|
+
const outputDetails = usage.output_tokens_details;
|
|
3366
|
+
const reportedInputTokens = usage.input_tokens ?? 0;
|
|
3367
|
+
const reportedOutputTokens = usage.output_tokens ?? 0;
|
|
3368
|
+
const reportedCachedTokens = details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
|
|
3369
|
+
const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
|
|
3370
|
+
const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
|
|
3371
|
+
const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
|
|
3372
|
+
const reportedTotalTokens = typeof usage.total_tokens === "number" ? usage.total_tokens : undefined;
|
|
3373
|
+
const reportedPrimaryTokens = reportedInputTokens + reportedOutputTokens;
|
|
3374
|
+
const reportedWithSeparateOrchestration =
|
|
3375
|
+
reportedPrimaryTokens + orchestrationInputTokens + orchestrationOutputTokens;
|
|
3376
|
+
const primaryIncludesOrchestration =
|
|
3377
|
+
reportedTotalTokens !== undefined &&
|
|
3378
|
+
orchestrationInputTokens + orchestrationOutputTokens > 0 &&
|
|
3379
|
+
Math.abs(reportedTotalTokens - reportedPrimaryTokens) <=
|
|
3380
|
+
Math.abs(reportedTotalTokens - reportedWithSeparateOrchestration);
|
|
3381
|
+
const orchestrationInputCached = Math.min(orchestrationInputTokens, orchestrationInputCachedTokens);
|
|
3382
|
+
const orchestrationInput = Math.max(0, orchestrationInputTokens - orchestrationInputCached);
|
|
3383
|
+
const accounting = calculateOpenAIUsageAccounting({
|
|
3384
|
+
promptTokens: Math.max(0, reportedInputTokens - (primaryIncludesOrchestration ? orchestrationInputTokens : 0)),
|
|
3385
|
+
outputTokens: Math.max(0, reportedOutputTokens - (primaryIncludesOrchestration ? orchestrationOutputTokens : 0)),
|
|
3386
|
+
cachedTokens: Math.max(0, reportedCachedTokens - (primaryIncludesOrchestration ? orchestrationInputCached : 0)),
|
|
3387
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
|
|
3388
|
+
cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
|
|
3389
|
+
cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
|
|
3390
|
+
hasDeepSeekCacheHitAndMiss:
|
|
3391
|
+
usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
|
|
3392
|
+
});
|
|
3393
|
+
const orchestrationTotal = orchestrationInput + orchestrationInputCached + orchestrationOutputTokens;
|
|
3394
|
+
if (orchestrationTotal > 0) {
|
|
3395
|
+
accounting.orchestration = {
|
|
3396
|
+
...(orchestrationInput > 0 ? { input: orchestrationInput } : {}),
|
|
3397
|
+
...(orchestrationInputCached > 0 ? { cacheRead: orchestrationInputCached } : {}),
|
|
3398
|
+
...(orchestrationOutputTokens > 0 ? { output: orchestrationOutputTokens } : {}),
|
|
3399
|
+
};
|
|
3400
|
+
accounting.totalTokens = reportedTotalTokens ?? accounting.totalTokens + orchestrationTotal;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
// Wholesale replacement must not drop provider-annotated extras (Copilot
|
|
3404
|
+
// premium-request accounting): the failed/cancelled paths throw right after
|
|
3405
|
+
// this call with no later chance to re-apply.
|
|
3406
|
+
const premiumRequests = output.usage.premiumRequests;
|
|
3407
|
+
output.usage = {
|
|
3408
|
+
...accounting,
|
|
3409
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
3410
|
+
};
|
|
3411
|
+
if (premiumRequests !== undefined) {
|
|
3412
|
+
output.usage.premiumRequests = premiumRequests;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
/**
|
|
3417
|
+
* Structural equality for the chain prefix/option check, equivalent to the
|
|
3418
|
+
* default {@link Bun.deepEquals} (own enumerable keys, `absent ≡ own-undefined`)
|
|
3419
|
+
* except for two deliberate exclusions:
|
|
3420
|
+
* - **symbol-keyed properties are ignored** — `for…in` walks enumerable
|
|
3421
|
+
* *string* keys only (never symbols); these are plain wire items whose
|
|
3422
|
+
* prototype contributes no enumerable keys, so iteration is effectively
|
|
3423
|
+
* own-string-keyed. That is how the transient streaming symbols
|
|
3424
|
+
* (`block-symbols.ts`) stamped onto live request items are excluded (the
|
|
3425
|
+
* deep-cloned baseline never carries them). Do NOT add an
|
|
3426
|
+
* `Object.getOwnPropertySymbols` pass, or those symbols resurface and break
|
|
3427
|
+
* chaining.
|
|
3428
|
+
* - keys listed in `omitKeys` are skipped (the option compare omits `input`
|
|
3429
|
+
* and the per-turn `client_metadata`).
|
|
3430
|
+
* A defined value differing across sides IS a difference; a key undefined or
|
|
3431
|
+
* absent on both stays equal. Nested values use full {@link Bun.deepEquals}.
|
|
3432
|
+
*/
|
|
3433
|
+
function deepEqualsWithout(a: unknown, b: unknown, omitKeys?: Record<string, boolean>): boolean {
|
|
3434
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return Bun.deepEquals(a, b);
|
|
3435
|
+
const ao = a as Record<string, unknown>;
|
|
3436
|
+
const bo = b as Record<string, unknown>;
|
|
3437
|
+
for (const key in ao) {
|
|
3438
|
+
if (omitKeys?.[key]) continue;
|
|
3439
|
+
const av = ao[key];
|
|
3440
|
+
const bv = bo[key];
|
|
3441
|
+
if (av !== bv && !Bun.deepEquals(av, bv)) return false;
|
|
3442
|
+
}
|
|
3443
|
+
for (const key in bo) {
|
|
3444
|
+
if (omitKeys?.[key]) continue;
|
|
3445
|
+
if (bo[key] !== undefined && !(key in ao)) return false;
|
|
3446
|
+
}
|
|
3447
|
+
return true;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
const TOP_LEVEL_EXCLUDE_MAP = {
|
|
3451
|
+
input: true,
|
|
3452
|
+
client_metadata: true,
|
|
3453
|
+
};
|
|
3454
|
+
|
|
3455
|
+
/**
|
|
3456
|
+
* Output-only lifecycle metadata excluded from per-item prefix identity:
|
|
3457
|
+
* replay sanitization strips `status` from message/function_call/custom
|
|
3458
|
+
* tool items (they reject output lifecycle fields), so raw response items
|
|
3459
|
+
* must not be distinguished from their sanitized replay form.
|
|
3460
|
+
*/
|
|
3461
|
+
const ITEM_LIFECYCLE_EXCLUDE_MAP = {
|
|
3462
|
+
status: true,
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
/**
|
|
3466
|
+
* Strict-prefix delta for stateful `previous_response_id` chaining (used by the
|
|
3467
|
+
* platform Responses provider and the Codex provider on both transports):
|
|
3468
|
+
* returns the input items the current request appends beyond the previous
|
|
3469
|
+
* request's input plus the previous response's output items, or null when the
|
|
3470
|
+
* request options differ or history mutated (the chain must break). Per-turn
|
|
3471
|
+
* `client_metadata` (e.g. rotating turn ids) is excluded from the option
|
|
3472
|
+
* comparison; codex-rs excludes it from the same check.
|
|
3473
|
+
*/
|
|
3474
|
+
export function buildResponsesDeltaInput<TItem extends ResponseInputItem | InputItem>(
|
|
3475
|
+
previous: { input?: TItem[] } | undefined,
|
|
3476
|
+
previousResponseItems: readonly TItem[] | undefined,
|
|
3477
|
+
current: { input?: TItem[] },
|
|
3478
|
+
): TItem[] | null {
|
|
3479
|
+
if (!previous) return null;
|
|
3480
|
+
if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
|
|
3481
|
+
if (!deepEqualsWithout(previous, current, TOP_LEVEL_EXCLUDE_MAP)) {
|
|
3482
|
+
return null;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
const baselineLen = (previous.input?.length ?? 0) + (previousResponseItems?.length ?? 0);
|
|
3486
|
+
if (current.input.length <= baselineLen) return null;
|
|
3487
|
+
|
|
3488
|
+
let index = 0;
|
|
3489
|
+
for (const series of [previous.input, previousResponseItems]) {
|
|
3490
|
+
if (!series) continue;
|
|
3491
|
+
for (const item of series) {
|
|
3492
|
+
if (deepEqualsWithout(item, current.input[index], ITEM_LIFECYCLE_EXCLUDE_MAP)) {
|
|
3493
|
+
index++;
|
|
3494
|
+
} else {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
return current.input.slice(index) as TItem[];
|
|
3500
|
+
}
|