jeopi-ai 16.2.13
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 +4347 -0
- package/README.md +1193 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +73 -0
- package/dist/types/auth-broker/discover.d.ts +35 -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 +102 -0
- package/dist/types/auth-broker/server.d.ts +43 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +107 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
- package/dist/types/auth-gateway/http.d.ts +39 -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 +123 -0
- package/dist/types/auth-retry.d.ts +124 -0
- package/dist/types/auth-storage.d.ts +1026 -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 +23 -0
- package/dist/types/dialect/examples.d.ts +2 -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 +12 -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 +45 -0
- package/dist/types/dialect/thinking.d.ts +6 -0
- package/dist/types/dialect/types.d.ts +69 -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 +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +79 -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 +13 -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 +59 -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 +49 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
- package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
- package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
- package/dist/types/providers/amazon-bedrock.d.ts +39 -0
- package/dist/types/providers/anthropic-client.d.ts +94 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic-wire.d.ts +318 -0
- package/dist/types/providers/anthropic.d.ts +248 -0
- package/dist/types/providers/aws-credentials.d.ts +53 -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/cursor.d.ts +91 -0
- package/dist/types/providers/devin.d.ts +12 -0
- package/dist/types/providers/error-message.d.ts +27 -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 +32 -0
- package/dist/types/providers/google-gemini-cli.d.ts +118 -0
- package/dist/types/providers/google-interactions.d.ts +65 -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 +178 -0
- package/dist/types/providers/ollama.d.ts +7 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-chat-wire.d.ts +644 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
- package/dist/types/providers/openai-codex-responses.d.ts +108 -0
- package/dist/types/providers/openai-completions.d.ts +45 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
- package/dist/types/providers/openai-responses.d.ts +126 -0
- package/dist/types/providers/openai-shared.d.ts +506 -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 +32 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +11 -0
- package/dist/types/providers/vision-guard.d.ts +20 -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/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/azure.d.ts +4 -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/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/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/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/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
- package/dist/types/registry/oauth/anthropic.d.ts +23 -0
- package/dist/types/registry/oauth/callback-server.d.ts +72 -0
- package/dist/types/registry/oauth/cursor.d.ts +15 -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 +10 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
- package/dist/types/registry/oauth/index.d.ts +64 -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 +56 -0
- package/dist/types/registry/oauth/wafer.d.ts +1 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
- package/dist/types/registry/oauth/xiaomi.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 +303 -0
- package/dist/types/registry/sakana.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 +51 -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 +4 -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 +7 -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 +44 -0
- package/dist/types/types.d.ts +715 -0
- package/dist/types/usage/claude.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 +2 -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 +79 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +346 -0
- package/dist/types/utils/abort.d.ts +25 -0
- package/dist/types/utils/anthropic-auth.d.ts +35 -0
- package/dist/types/utils/block-symbols.d.ts +20 -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 +30 -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 +118 -0
- package/dist/types/utils/http-inspector.d.ts +30 -0
- package/dist/types/utils/idle-iterator.d.ts +137 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
- package/dist/types/utils/openai-http.d.ts +54 -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 +29 -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 +15 -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 +124 -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 +18 -0
- package/dist/types/utils/schema/wire.d.ts +92 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -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 +50 -0
- package/dist/types/utils/validation.d.ts +42 -0
- package/dist/types/utils.d.ts +24 -0
- package/package.json +139 -0
- package/src/api-registry.ts +109 -0
- package/src/auth-broker/client.ts +359 -0
- package/src/auth-broker/discover.ts +222 -0
- package/src/auth-broker/index.ts +7 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +657 -0
- package/src/auth-broker/server.ts +646 -0
- package/src/auth-broker/snapshot-cache.ts +191 -0
- package/src/auth-broker/types.ts +130 -0
- package/src/auth-broker/wire-schemas.ts +249 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +802 -0
- package/src/auth-gateway/types.ts +151 -0
- package/src/auth-retry.ts +250 -0
- package/src/auth-storage.ts +5576 -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 +36 -0
- package/src/dialect/examples.ts +33 -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 +597 -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 +456 -0
- package/src/dialect/harmony.md +31 -0
- package/src/dialect/harmony.ts +346 -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 +73 -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 +470 -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 +249 -0
- package/src/dialect/thinking.ts +122 -0
- package/src/dialect/types.ts +57 -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 +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +506 -0
- package/src/error/format.ts +45 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +62 -0
- package/src/error/rate-limit.ts +161 -0
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +49 -0
- package/src/provider-details.ts +90 -0
- package/src/providers/__tests__/google-auth.test.ts +144 -0
- package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
- package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
- package/src/providers/amazon-bedrock.ts +1042 -0
- package/src/providers/anthropic-client.ts +295 -0
- package/src/providers/anthropic-messages-server-schema.ts +252 -0
- package/src/providers/anthropic-messages-server.ts +756 -0
- package/src/providers/anthropic-wire.ts +318 -0
- package/src/providers/anthropic.ts +4078 -0
- package/src/providers/aws-credentials.ts +586 -0
- package/src/providers/aws-eventstream.ts +181 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +382 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2695 -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 +577 -0
- package/src/providers/error-message.ts +21 -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 +3058 -0
- package/src/providers/gitlab-duo.ts +395 -0
- package/src/providers/google-auth.ts +350 -0
- package/src/providers/google-gemini-cli.ts +1362 -0
- package/src/providers/google-interactions.ts +753 -0
- package/src/providers/google-shared.ts +1103 -0
- package/src/providers/google-types.ts +180 -0
- package/src/providers/google-vertex.ts +183 -0
- package/src/providers/google.ts +87 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +507 -0
- package/src/providers/ollama.ts +773 -0
- package/src/providers/openai-anthropic-shim.ts +152 -0
- package/src/providers/openai-chat-server-schema.ts +242 -0
- package/src/providers/openai-chat-server.ts +715 -0
- package/src/providers/openai-chat-wire.ts +847 -0
- package/src/providers/openai-codex/request-transformer.ts +295 -0
- package/src/providers/openai-codex/response-handler.ts +102 -0
- package/src/providers/openai-codex-responses.ts +3468 -0
- package/src/providers/openai-completions.ts +2173 -0
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses-reasoning-suppression.md +1 -0
- package/src/providers/openai-responses-server-schema.ts +282 -0
- package/src/providers/openai-responses-server.ts +1280 -0
- package/src/providers/openai-responses-wire.ts +6391 -0
- package/src/providers/openai-responses.ts +1022 -0
- package/src/providers/openai-shared.ts +2648 -0
- package/src/providers/pi-native-client.ts +266 -0
- package/src/providers/pi-native-server.ts +242 -0
- package/src/providers/register-builtins.ts +475 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +787 -0
- package/src/providers/vision-guard.ts +54 -0
- package/src/registry/aimlapi.ts +6 -0
- package/src/registry/alibaba-coding-plan.ts +95 -0
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/registry/api-key-login.ts +112 -0
- package/src/registry/api-key-validation.ts +161 -0
- package/src/registry/azure.ts +6 -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/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/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/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 +22 -0
- package/src/registry/nanogpt.ts +22 -0
- package/src/registry/nvidia.ts +61 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
- package/src/registry/oauth/anthropic.ts +311 -0
- package/src/registry/oauth/callback-server.ts +315 -0
- package/src/registry/oauth/cursor.ts +171 -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 +209 -0
- package/src/registry/oauth/google-gemini-cli.ts +273 -0
- package/src/registry/oauth/google-oauth-shared.ts +125 -0
- package/src/registry/oauth/index.ts +269 -0
- package/src/registry/oauth/kimi.ts +289 -0
- package/src/registry/oauth/minimax-code.ts +53 -0
- package/src/registry/oauth/oauth.html +311 -0
- package/src/registry/oauth/openai-codex.ts +364 -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 +65 -0
- package/src/registry/oauth/wafer.ts +24 -0
- package/src/registry/oauth/xai-oauth.ts +394 -0
- package/src/registry/oauth/xiaomi.ts +211 -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 +161 -0
- package/src/registry/sakana.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 +56 -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 +6 -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 +27 -0
- package/src/registry/zenmux.ts +22 -0
- package/src/registry/zhipu-coding-plan.ts +27 -0
- package/src/stream.ts +1778 -0
- package/src/types.ts +856 -0
- package/src/usage/claude.ts +485 -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 +271 -0
- package/src/usage/minimax-code.ts +30 -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 +174 -0
- package/src/usage/openai-codex.ts +535 -0
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +321 -0
- package/src/usage.ts +333 -0
- package/src/utils/abort.ts +67 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/empty-completion-retry.ts +159 -0
- package/src/utils/event-stream.ts +171 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/google-validation.ts +25 -0
- package/src/utils/harmony-leak.ts +456 -0
- package/src/utils/http-inspector.ts +168 -0
- package/src/utils/idle-iterator.ts +473 -0
- package/src/utils/leaked-thinking-stream.ts +294 -0
- package/src/utils/openai-http.ts +122 -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 +240 -0
- package/src/utils/request-debug.ts +351 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +59 -0
- package/src/utils/schema/CONSTRAINTS.md +166 -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 +207 -0
- package/src/utils/schema/index.ts +15 -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 +1901 -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 +198 -0
- package/src/utils/schema/wire.ts +789 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -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 +99 -0
- package/src/utils/validation.ts +1507 -0
- package/src/utils.ts +171 -0
|
@@ -0,0 +1,2648 @@
|
|
|
1
|
+
import type { Effort } from "jeopi-catalog/effort";
|
|
2
|
+
import { toFirepassWireModelId, toFireworksWireModelId } from "jeopi-catalog/fireworks-model-id";
|
|
3
|
+
import { isGlm52ReasoningEffortModelId } from "jeopi-catalog/identity";
|
|
4
|
+
import { getSupportedEfforts } from "jeopi-catalog/model-thinking";
|
|
5
|
+
import { calculateCost } from "jeopi-catalog/models";
|
|
6
|
+
import type {
|
|
7
|
+
OpenAICompat,
|
|
8
|
+
OpenAIReasoningDisableMode,
|
|
9
|
+
OpenAIStreamMarkupHealingPattern,
|
|
10
|
+
OpenRouterRouting,
|
|
11
|
+
ResolvedOpenAICompat,
|
|
12
|
+
ResolvedOpenAIResponsesCompat,
|
|
13
|
+
ResolvedOpenAISharedCompat,
|
|
14
|
+
VercelGatewayRouting,
|
|
15
|
+
} from "jeopi-catalog/types";
|
|
16
|
+
import {
|
|
17
|
+
COREWEAVE_PROJECT_HEADER,
|
|
18
|
+
coreWeaveProjectHeaders,
|
|
19
|
+
hasCoreWeaveProjectHeader,
|
|
20
|
+
removeBlankCoreWeaveProjectHeaders,
|
|
21
|
+
} from "jeopi-catalog/wire/coreweave";
|
|
22
|
+
import { parseGitHubCopilotApiKey } from "jeopi-catalog/wire/github-copilot";
|
|
23
|
+
import {
|
|
24
|
+
$env,
|
|
25
|
+
extractHttpStatusFromError,
|
|
26
|
+
logger,
|
|
27
|
+
parseStreamingJson,
|
|
28
|
+
parseStreamingJsonThrottled,
|
|
29
|
+
structuredCloneJSON,
|
|
30
|
+
} from "jeopi-utils";
|
|
31
|
+
import * as AIError from "../error";
|
|
32
|
+
import {
|
|
33
|
+
type Api,
|
|
34
|
+
type AssistantMessage,
|
|
35
|
+
type CacheRetention,
|
|
36
|
+
type Context,
|
|
37
|
+
type ImageContent,
|
|
38
|
+
type Message,
|
|
39
|
+
type MessageAttribution,
|
|
40
|
+
type Model,
|
|
41
|
+
OPENAI_MAX_OUTPUT_TOKENS,
|
|
42
|
+
type Provider,
|
|
43
|
+
type ServiceTier,
|
|
44
|
+
type StopReason,
|
|
45
|
+
type StreamOptions,
|
|
46
|
+
shouldSendServiceTier,
|
|
47
|
+
type TextContent,
|
|
48
|
+
type TextSignatureV1,
|
|
49
|
+
type ThinkingContent,
|
|
50
|
+
type Tool,
|
|
51
|
+
type ToolCall,
|
|
52
|
+
type ToolResultMessage,
|
|
53
|
+
} from "../types";
|
|
54
|
+
import {
|
|
55
|
+
getOpenAIResponsesHistoryItems,
|
|
56
|
+
getOpenAIResponsesHistoryPayload,
|
|
57
|
+
normalizeResponsesToolCallId,
|
|
58
|
+
normalizeSystemPrompts,
|
|
59
|
+
resolveCacheRetention,
|
|
60
|
+
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
61
|
+
} from "../utils";
|
|
62
|
+
import {
|
|
63
|
+
clearStreamingPartialJson,
|
|
64
|
+
kStreamingArgumentsDone,
|
|
65
|
+
kStreamingLastParseLen,
|
|
66
|
+
kStreamingPartialJson,
|
|
67
|
+
} from "../utils/block-symbols";
|
|
68
|
+
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
69
|
+
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
70
|
+
import { getOpenRouterHeaders } from "../utils/openrouter-headers";
|
|
71
|
+
import { isForcedToolChoice } from "../utils/tool-choice";
|
|
72
|
+
import {
|
|
73
|
+
buildCopilotDynamicHeaders,
|
|
74
|
+
hasCopilotVisionInput,
|
|
75
|
+
resolveGitHubCopilotBaseUrl,
|
|
76
|
+
} from "./github-copilot-headers";
|
|
77
|
+
import type { ChatCompletionCreateParamsStreaming } from "./openai-chat-wire";
|
|
78
|
+
import type { InputItem } from "./openai-codex/request-transformer";
|
|
79
|
+
import responsesReasoningSuppressionPrompt from "./openai-responses-reasoning-suppression.md" with { type: "text" };
|
|
80
|
+
import type {
|
|
81
|
+
ResponseContentPartAddedEvent,
|
|
82
|
+
ResponseCreateParamsStreaming,
|
|
83
|
+
ResponseCustomToolCall,
|
|
84
|
+
ResponseFunctionToolCall,
|
|
85
|
+
ResponseInput,
|
|
86
|
+
ResponseInputContent,
|
|
87
|
+
ResponseInputImage,
|
|
88
|
+
ResponseInputItem,
|
|
89
|
+
ResponseInputText,
|
|
90
|
+
ResponseOutputItem,
|
|
91
|
+
ResponseOutputMessage,
|
|
92
|
+
ResponseReasoningItem,
|
|
93
|
+
ResponseStatus,
|
|
94
|
+
ResponseStreamEvent,
|
|
95
|
+
} from "./openai-responses-wire";
|
|
96
|
+
import { transformMessages } from "./transform-messages";
|
|
97
|
+
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
|
|
98
|
+
|
|
99
|
+
export interface OpenAIModelIdentity {
|
|
100
|
+
provider: string;
|
|
101
|
+
id: string;
|
|
102
|
+
baseUrl?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface OpenAIStrictToolsScope {
|
|
106
|
+
provider: string;
|
|
107
|
+
baseUrl: string | undefined;
|
|
108
|
+
modelId: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface OpenAIStrictToolsState {
|
|
112
|
+
strictTools: {
|
|
113
|
+
disabledModelScopes: Set<string>;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface OpenAIRequestSetupModel extends OpenAIModelIdentity {
|
|
118
|
+
headers?: Record<string, string>;
|
|
119
|
+
premiumMultiplier?: number;
|
|
120
|
+
compat?: Pick<ResolvedOpenAISharedCompat, "promptCacheSessionHeader">;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface OpenAIResponsesCacheOptions {
|
|
124
|
+
cacheRetention?: CacheRetention;
|
|
125
|
+
sessionId?: string;
|
|
126
|
+
promptCacheKey?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface OpenAIRequestSetupOptions {
|
|
130
|
+
apiKey?: string;
|
|
131
|
+
extraHeaders?: Record<string, string>;
|
|
132
|
+
initiatorOverride?: MessageAttribution;
|
|
133
|
+
messages: Message[];
|
|
134
|
+
defaultBaseUrl?: string;
|
|
135
|
+
prependHeaders?: () => Record<string, string>;
|
|
136
|
+
alibabaCodingPlanAuth?: boolean;
|
|
137
|
+
azureChatCompletions?: {
|
|
138
|
+
apiVersion: string;
|
|
139
|
+
deploymentName: string;
|
|
140
|
+
};
|
|
141
|
+
openAISessionId?: string;
|
|
142
|
+
promptCacheSessionId?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface OpenAIRequestSetup {
|
|
146
|
+
copilotPremiumRequests: number | undefined;
|
|
147
|
+
baseUrl: string | undefined;
|
|
148
|
+
headers: Record<string, string>;
|
|
149
|
+
query: Record<string, string> | undefined;
|
|
150
|
+
requestHeaders: Record<string, string>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
154
|
+
const value = baseUrl?.trim();
|
|
155
|
+
if (!value) return undefined;
|
|
156
|
+
const normalized = value.replace(/\/+$/, "");
|
|
157
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveSakanaRequestBaseUrl(): string | undefined {
|
|
161
|
+
return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyCoreWeaveProjectHeader(headers: Record<string, string>): void {
|
|
165
|
+
removeBlankCoreWeaveProjectHeaders(headers);
|
|
166
|
+
if (hasCoreWeaveProjectHeader(headers)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const projectHeaders = coreWeaveProjectHeaders($env);
|
|
170
|
+
if (projectHeaders) {
|
|
171
|
+
headers[COREWEAVE_PROJECT_HEADER] = projectHeaders[COREWEAVE_PROJECT_HEADER];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function resolveOpenAIRequestSetup(
|
|
176
|
+
model: OpenAIRequestSetupModel,
|
|
177
|
+
options: OpenAIRequestSetupOptions,
|
|
178
|
+
): OpenAIRequestSetup {
|
|
179
|
+
let apiKey = options.apiKey;
|
|
180
|
+
if (!apiKey) {
|
|
181
|
+
if (!$env.OPENAI_API_KEY) {
|
|
182
|
+
throw new AIError.MissingApiKeyError(
|
|
183
|
+
undefined,
|
|
184
|
+
"OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
apiKey = $env.OPENAI_API_KEY;
|
|
188
|
+
}
|
|
189
|
+
const rawApiKey = apiKey;
|
|
190
|
+
let headers = { ...(model.headers ?? {}) };
|
|
191
|
+
if (model.provider === "openrouter") {
|
|
192
|
+
Object.assign(headers, getOpenRouterHeaders());
|
|
193
|
+
}
|
|
194
|
+
Object.assign(headers, options.extraHeaders);
|
|
195
|
+
if (model.provider === "coreweave") {
|
|
196
|
+
applyCoreWeaveProjectHeader(headers);
|
|
197
|
+
}
|
|
198
|
+
if (options.prependHeaders) {
|
|
199
|
+
headers = { ...options.prependHeaders(), ...headers };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let copilotPremiumRequests: number | undefined;
|
|
203
|
+
let baseUrl = model.baseUrl;
|
|
204
|
+
if (model.provider === "moonshot") {
|
|
205
|
+
// Bundled `moonshot` catalog models hardcode the international endpoint
|
|
206
|
+
// (`api.moonshot.ai`). MOONSHOT_BASE_URL lets users redirect the provider
|
|
207
|
+
// at the China platform (`api.moonshot.cn`), which only accepts China keys
|
|
208
|
+
// and rejects the international host. (#2883)
|
|
209
|
+
const moonshotBaseUrl = $env.MOONSHOT_BASE_URL?.trim();
|
|
210
|
+
if (moonshotBaseUrl) {
|
|
211
|
+
baseUrl = moonshotBaseUrl;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (model.provider === "sakana") {
|
|
215
|
+
const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
|
|
216
|
+
if (sakanaBaseUrl) {
|
|
217
|
+
baseUrl = sakanaBaseUrl;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (model.provider === "github-copilot") {
|
|
221
|
+
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
222
|
+
const copilot = buildCopilotDynamicHeaders({
|
|
223
|
+
messages: options.messages,
|
|
224
|
+
hasImages: hasCopilotVisionInput(options.messages),
|
|
225
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
226
|
+
headers,
|
|
227
|
+
initiatorOverride: options.initiatorOverride,
|
|
228
|
+
});
|
|
229
|
+
Object.assign(headers, copilot.headers);
|
|
230
|
+
copilotPremiumRequests = copilot.premiumRequests;
|
|
231
|
+
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (options.alibabaCodingPlanAuth && model.provider === "alibaba-coding-plan") {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(rawApiKey);
|
|
237
|
+
if (typeof parsed?.token === "string") {
|
|
238
|
+
apiKey = parsed.token;
|
|
239
|
+
}
|
|
240
|
+
if (typeof parsed?.enterpriseUrl === "string") {
|
|
241
|
+
baseUrl = parsed.enterpriseUrl;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Not JSON — use raw apiKey and catalog baseUrl.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let query: Record<string, string> | undefined;
|
|
249
|
+
if (options.azureChatCompletions && baseUrl?.includes(".openai.azure.com")) {
|
|
250
|
+
if (!baseUrl.includes("/deployments/")) {
|
|
251
|
+
baseUrl = `${baseUrl}/deployments/${options.azureChatCompletions.deploymentName}`;
|
|
252
|
+
}
|
|
253
|
+
query = { "api-version": options.azureChatCompletions.apiVersion };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (options.openAISessionId && model.provider === "openai") {
|
|
257
|
+
headers.session_id ??= options.openAISessionId;
|
|
258
|
+
headers["x-client-request-id"] ??= options.openAISessionId;
|
|
259
|
+
}
|
|
260
|
+
if (options.promptCacheSessionId && model.compat?.promptCacheSessionHeader) {
|
|
261
|
+
headers[model.compat.promptCacheSessionHeader] ??= options.promptCacheSessionId;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (options.defaultBaseUrl !== undefined) {
|
|
265
|
+
baseUrl = baseUrl ?? ($env.OPENAI_BASE_URL?.trim() || options.defaultBaseUrl);
|
|
266
|
+
}
|
|
267
|
+
const requestHeaders = { ...headers };
|
|
268
|
+
headers.Authorization ??= `Bearer ${apiKey}`;
|
|
269
|
+
return { copilotPremiumRequests, baseUrl, headers, query, requestHeaders };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function applyOpenAIServiceTier(
|
|
273
|
+
params: { service_tier?: ServiceTier | null | undefined },
|
|
274
|
+
serviceTier: ServiceTier | null | undefined,
|
|
275
|
+
provider: Provider | undefined,
|
|
276
|
+
): void {
|
|
277
|
+
if (!shouldSendServiceTier(serviceTier, provider)) return;
|
|
278
|
+
if (serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority") {
|
|
279
|
+
params.service_tier = serviceTier;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Standard OpenAI Responses service-tier cost multipliers. The non-Codex
|
|
285
|
+
* Responses path bills the tier it was served (or requested): Flex processing is
|
|
286
|
+
* half price; Priority is a 2x premium. Codex bills the same tiers with its own
|
|
287
|
+
* table (Priority is 2.5x on gpt-5.5) and applies that separately.
|
|
288
|
+
*/
|
|
289
|
+
function getOpenAIResponsesServiceTierCostMultiplier(tier: string | null | undefined): number {
|
|
290
|
+
switch (tier) {
|
|
291
|
+
case "flex":
|
|
292
|
+
return 0.5;
|
|
293
|
+
case "priority":
|
|
294
|
+
return 2;
|
|
295
|
+
default:
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Adjust resolved cost by the service tier OpenAI actually billed — parity with
|
|
302
|
+
* Codex (`applyCodexServiceTierPricing`), but with the standard (non-Codex)
|
|
303
|
+
* multipliers. The served tier comes from the response echo, falling back to the
|
|
304
|
+
* resolved request tier. Scoped to `provider: "openai"` (the only standard
|
|
305
|
+
* Responses biller) so an echoed `service_tier` from an Azure/OpenRouter/Copilot
|
|
306
|
+
* proxy can never skew those costs.
|
|
307
|
+
*/
|
|
308
|
+
export function applyOpenAIResponsesServiceTierCost(
|
|
309
|
+
model: Pick<Model, "provider">,
|
|
310
|
+
usage: AssistantMessage["usage"],
|
|
311
|
+
responseServiceTier: unknown,
|
|
312
|
+
requestServiceTier: ServiceTier | null | undefined,
|
|
313
|
+
): void {
|
|
314
|
+
if (model.provider !== "openai") return;
|
|
315
|
+
// The response echo is authoritative when present (OpenAI may downgrade a
|
|
316
|
+
// requested priority/flex turn to default under load); only fall back to the
|
|
317
|
+
// requested tier when the response omits the echo entirely.
|
|
318
|
+
const served = typeof responseServiceTier === "string" ? responseServiceTier : (requestServiceTier ?? undefined);
|
|
319
|
+
const multiplier = getOpenAIResponsesServiceTierCostMultiplier(served);
|
|
320
|
+
if (multiplier === 1) return;
|
|
321
|
+
usage.cost.input *= multiplier;
|
|
322
|
+
usage.cost.output *= multiplier;
|
|
323
|
+
usage.cost.cacheRead *= multiplier;
|
|
324
|
+
usage.cost.cacheWrite *= multiplier;
|
|
325
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface OpenAIUsageAccountingInput {
|
|
329
|
+
promptTokens: number;
|
|
330
|
+
outputTokens: number;
|
|
331
|
+
cachedTokens: number;
|
|
332
|
+
reasoningTokens: number;
|
|
333
|
+
cacheWriteOpenRouter: number | undefined;
|
|
334
|
+
cacheWriteDeepSeek: number | undefined;
|
|
335
|
+
hasDeepSeekCacheHitAndMiss: boolean;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface OpenAIUsageAccounting {
|
|
339
|
+
input: number;
|
|
340
|
+
output: number;
|
|
341
|
+
cacheRead: number;
|
|
342
|
+
cacheWrite: number;
|
|
343
|
+
totalTokens: number;
|
|
344
|
+
reasoningTokens?: number;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting {
|
|
348
|
+
const cacheWriteTokens = accounting.cacheWriteOpenRouter ?? accounting.cacheWriteDeepSeek ?? 0;
|
|
349
|
+
const isDeepSeekUsage =
|
|
350
|
+
accounting.hasDeepSeekCacheHitAndMiss &&
|
|
351
|
+
accounting.cacheWriteOpenRouter === undefined &&
|
|
352
|
+
(accounting.cacheWriteDeepSeek ?? 0) > 0;
|
|
353
|
+
const input = isDeepSeekUsage
|
|
354
|
+
? Math.max(0, accounting.promptTokens - accounting.cachedTokens)
|
|
355
|
+
: Math.max(0, accounting.promptTokens - accounting.cachedTokens - cacheWriteTokens);
|
|
356
|
+
const cacheWrite = isDeepSeekUsage ? 0 : cacheWriteTokens;
|
|
357
|
+
return {
|
|
358
|
+
input,
|
|
359
|
+
output: accounting.outputTokens,
|
|
360
|
+
cacheRead: accounting.cachedTokens,
|
|
361
|
+
cacheWrite,
|
|
362
|
+
totalTokens: input + accounting.outputTokens + accounting.cachedTokens + cacheWrite,
|
|
363
|
+
...(accounting.reasoningTokens > 0 ? { reasoningTokens: accounting.reasoningTokens } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function normalizeOpenAIResponsesPromptCacheKey(sessionId: string | undefined): string | undefined {
|
|
368
|
+
return normalizeOpenAIStableId(sessionId, 64, "pc_");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function normalizeOpenRouterResponsesSessionId(sessionId: string | undefined): string | undefined {
|
|
372
|
+
return normalizeOpenAIStableId(sessionId, 256, "session_");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function getOpenAIResponsesPromptCacheKey(options: OpenAIResponsesCacheOptions | undefined): string | undefined {
|
|
376
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
377
|
+
return normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function getOpenAIResponsesRoutingSessionId(
|
|
381
|
+
options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined,
|
|
382
|
+
): string | undefined {
|
|
383
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
384
|
+
return normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function getOpenRouterResponsesSessionId(
|
|
388
|
+
options: Pick<OpenAIResponsesCacheOptions, "cacheRetention" | "sessionId"> | undefined,
|
|
389
|
+
): string | undefined {
|
|
390
|
+
if (resolveCacheRetention(options?.cacheRetention) === "none") return undefined;
|
|
391
|
+
return normalizeOpenRouterResponsesSessionId(options?.sessionId);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function parseAzureDeploymentNameMap(value: string | undefined): Map<string, string> {
|
|
395
|
+
const map = new Map<string, string>();
|
|
396
|
+
if (!value) return map;
|
|
397
|
+
for (const entry of value.split(",")) {
|
|
398
|
+
const trimmed = entry.trim();
|
|
399
|
+
if (!trimmed) continue;
|
|
400
|
+
const [modelId, deploymentName] = trimmed.split("=", 2);
|
|
401
|
+
if (!modelId || !deploymentName) continue;
|
|
402
|
+
map.set(modelId.trim(), deploymentName.trim());
|
|
403
|
+
}
|
|
404
|
+
return map;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function createOpenAIStrictToolsState(): OpenAIStrictToolsState {
|
|
408
|
+
return {
|
|
409
|
+
strictTools: {
|
|
410
|
+
disabledModelScopes: new Set<string>(),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function clearOpenAIStrictToolsState(state: OpenAIStrictToolsState): void {
|
|
416
|
+
state.strictTools.disabledModelScopes.clear();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function getOpenAIStrictToolsScope(
|
|
420
|
+
model: OpenAIModelIdentity,
|
|
421
|
+
resolvedBaseUrl: string | undefined,
|
|
422
|
+
): OpenAIStrictToolsScope {
|
|
423
|
+
return {
|
|
424
|
+
provider: model.provider,
|
|
425
|
+
baseUrl: resolvedBaseUrl ?? model.baseUrl,
|
|
426
|
+
modelId: model.id,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function isStrictToolsDisabledForScope(
|
|
431
|
+
state: OpenAIStrictToolsState | undefined,
|
|
432
|
+
scope: OpenAIStrictToolsScope | undefined,
|
|
433
|
+
): boolean {
|
|
434
|
+
if (!scope) return false;
|
|
435
|
+
return (
|
|
436
|
+
state?.strictTools.disabledModelScopes.has(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`) ?? false
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function disableStrictToolsForScope(
|
|
441
|
+
state: OpenAIStrictToolsState | undefined,
|
|
442
|
+
scope: OpenAIStrictToolsScope | undefined,
|
|
443
|
+
): void {
|
|
444
|
+
if (!scope) return;
|
|
445
|
+
state?.strictTools.disabledModelScopes.add(`${scope.provider}:${scope.baseUrl ?? ""}:${scope.modelId}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function isOpenRouterAnthropicModel(model: OpenAIModelIdentity): boolean {
|
|
449
|
+
return model.provider === "openrouter" && model.id.toLowerCase().startsWith("anthropic/");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Append an OpenRouter routing-variant suffix (e.g. `:nitro`, `:floor`, `:online`, `:exacto`)
|
|
454
|
+
* to a model id when no explicit variant is already present. A variant is considered
|
|
455
|
+
* "already present" when `modelId` contains a colon after the last `/` separator —
|
|
456
|
+
* which covers both user-typed selectors (`anthropic/claude-haiku:nitro`) and catalog
|
|
457
|
+
* entries that bake the variant in (`deepseek/deepseek-v3.1-terminus:exacto`).
|
|
458
|
+
*/
|
|
459
|
+
export function applyOpenRouterRoutingVariant(modelId: string, variant: string | undefined): string {
|
|
460
|
+
if (!variant) return modelId;
|
|
461
|
+
const lastSlash = modelId.lastIndexOf("/");
|
|
462
|
+
const lastColon = modelId.lastIndexOf(":");
|
|
463
|
+
if (lastColon > lastSlash) return modelId;
|
|
464
|
+
return `${modelId}:${variant}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function applyWireModelIdTransform(
|
|
468
|
+
baseId: string,
|
|
469
|
+
mode: ResolvedOpenAISharedCompat["wireModelIdMode"],
|
|
470
|
+
openrouterVariant?: string,
|
|
471
|
+
): string {
|
|
472
|
+
switch (mode) {
|
|
473
|
+
case "firepass":
|
|
474
|
+
return toFirepassWireModelId(baseId);
|
|
475
|
+
case "fireworks":
|
|
476
|
+
return toFireworksWireModelId(baseId);
|
|
477
|
+
case "openrouter":
|
|
478
|
+
return applyOpenRouterRoutingVariant(baseId, openrouterVariant);
|
|
479
|
+
default:
|
|
480
|
+
return baseId;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export interface OpenAIOutputTokenParam {
|
|
485
|
+
field: "max_tokens" | "max_completion_tokens" | "max_output_tokens";
|
|
486
|
+
value: number;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export interface ResolveOpenAIOutputTokenInput {
|
|
490
|
+
/** Wire field the endpoint expects for the output cap. */
|
|
491
|
+
field: OpenAIOutputTokenParam["field"];
|
|
492
|
+
/** Caller-supplied output cap (model-defaulted by `stream.ts`, or null/undefined on direct provider calls). */
|
|
493
|
+
maxTokens: number | null | undefined;
|
|
494
|
+
/** Whether the caller explicitly set `maxTokens` (routing omission only applies when false). */
|
|
495
|
+
maxTokensExplicit: boolean;
|
|
496
|
+
/** Model output cap (`model.maxTokens`). */
|
|
497
|
+
modelMaxTokens: number | null | undefined;
|
|
498
|
+
/** Drop the field entirely — proxies with unknown upstream caps (Ollama via `model.omitMaxOutputTokens`). */
|
|
499
|
+
omitMaxOutputTokens: boolean;
|
|
500
|
+
/** The model sits behind OpenRouter (catalog default caps are omitted so each upstream self-caps). */
|
|
501
|
+
isOpenRouterHost: boolean;
|
|
502
|
+
/** Endpoint always needs a cap (Kimi-family TPM math); supplies the model default when the caller did not. */
|
|
503
|
+
alwaysSendMaxTokens: boolean;
|
|
504
|
+
/** Hard provider clamp; defaults to {@link OPENAI_MAX_OUTPUT_TOKENS}. */
|
|
505
|
+
providerOutputClamp?: number;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Resolve the single output-token wire parameter shared by Chat Completions
|
|
510
|
+
* (`max_tokens`/`max_completion_tokens`) and the Responses family
|
|
511
|
+
* (`max_output_tokens`). Centralizes the provider exceptions that previously
|
|
512
|
+
* lived inline in both `buildParams`:
|
|
513
|
+
* - `alwaysSendMaxTokens`: Kimi-family endpoints derive TPM limits from the
|
|
514
|
+
* cap and require one on every call, so default from the model cap (or
|
|
515
|
+
* {@link OPENAI_MAX_OUTPUT_TOKENS}) when the caller omitted it.
|
|
516
|
+
* - OpenRouter routing omission: OpenRouter fans out to upstreams whose output
|
|
517
|
+
* caps differ from the catalog value, so a catalog default above the routed
|
|
518
|
+
* upstream's cap makes OpenRouter skip that upstream. Omit catalog defaults
|
|
519
|
+
* (explicit caller caps still win) so `provider.order`/`only` is honored.
|
|
520
|
+
* - model/provider clamp: never exceed `model.maxTokens` or the provider clamp
|
|
521
|
+
* (`OPENAI_MAX_OUTPUT_TOKENS`, raised for GLM-5.2 reasoning by the caller).
|
|
522
|
+
* - `omitMaxOutputTokens`: proxies (Ollama) with unknown upstream caps drop it.
|
|
523
|
+
*/
|
|
524
|
+
export function resolveOpenAIOutputTokenParam(
|
|
525
|
+
input: ResolveOpenAIOutputTokenInput,
|
|
526
|
+
): OpenAIOutputTokenParam | undefined {
|
|
527
|
+
if (input.omitMaxOutputTokens) return undefined;
|
|
528
|
+
const requested =
|
|
529
|
+
input.maxTokens ?? (input.alwaysSendMaxTokens ? (input.modelMaxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined);
|
|
530
|
+
if (requested === undefined) return undefined;
|
|
531
|
+
if (input.isOpenRouterHost && !input.alwaysSendMaxTokens && !input.maxTokensExplicit) return undefined;
|
|
532
|
+
const value = Math.min(
|
|
533
|
+
requested,
|
|
534
|
+
input.modelMaxTokens ?? Number.POSITIVE_INFINITY,
|
|
535
|
+
input.providerOutputClamp ?? OPENAI_MAX_OUTPUT_TOKENS,
|
|
536
|
+
);
|
|
537
|
+
if (!(value > 0)) return undefined;
|
|
538
|
+
return { field: input.field, value };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface OpenAIGatewayRoutingParams {
|
|
542
|
+
provider?: OpenRouterRouting;
|
|
543
|
+
providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export interface OpenAIGatewayRoutingCompat {
|
|
547
|
+
isOpenRouterHost: boolean;
|
|
548
|
+
openRouterRouting?: OpenRouterRouting;
|
|
549
|
+
isVercelGatewayHost?: boolean;
|
|
550
|
+
vercelGatewayRouting?: VercelGatewayRouting;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Apply gateway routing preferences to the request body. OpenRouter routes via
|
|
555
|
+
* the top-level `provider` field; the Vercel AI Gateway routes via
|
|
556
|
+
* `providerOptions.gateway`. Both Chat Completions and Responses call this; the
|
|
557
|
+
* Vercel branch is inert for Responses, whose resolved compat never sets
|
|
558
|
+
* `isVercelGatewayHost`.
|
|
559
|
+
*/
|
|
560
|
+
export function applyOpenAIGatewayRouting(
|
|
561
|
+
params: OpenAIGatewayRoutingParams,
|
|
562
|
+
compat: OpenAIGatewayRoutingCompat,
|
|
563
|
+
): void {
|
|
564
|
+
if (compat.isOpenRouterHost && compat.openRouterRouting) {
|
|
565
|
+
params.provider = compat.openRouterRouting;
|
|
566
|
+
}
|
|
567
|
+
if (compat.isVercelGatewayHost && compat.vercelGatewayRouting) {
|
|
568
|
+
const routing = compat.vercelGatewayRouting;
|
|
569
|
+
if (routing.only || routing.order) {
|
|
570
|
+
const gatewayOptions: { only?: string[]; order?: string[] } = {};
|
|
571
|
+
if (routing.only) gatewayOptions.only = routing.only;
|
|
572
|
+
if (routing.order) gatewayOptions.order = routing.order;
|
|
573
|
+
params.providerOptions = { gateway: gatewayOptions };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export interface OpenAIExtraBodyOptions {
|
|
579
|
+
/**
|
|
580
|
+
* Fireworks rejects DeepSeek-style `thinking` toggles alongside OpenAI-style
|
|
581
|
+
* `reasoning_effort`; drop `thinking` when the effort field carries the level.
|
|
582
|
+
*/
|
|
583
|
+
dropThinkingWhenReasoningEffort?: boolean;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Merge a compat/options `extraBody` blob into the request params. When
|
|
588
|
+
* `dropThinkingWhenReasoningEffort` is set and `reasoning_effort` is present,
|
|
589
|
+
* delete the conflicting `thinking` toggle (Fireworks rejects both together).
|
|
590
|
+
*/
|
|
591
|
+
export function applyOpenAIExtraBody<P extends object>(
|
|
592
|
+
params: P,
|
|
593
|
+
extraBody: Record<string, unknown> | undefined,
|
|
594
|
+
options?: OpenAIExtraBodyOptions,
|
|
595
|
+
): void {
|
|
596
|
+
if (!extraBody) return;
|
|
597
|
+
Object.assign(params, extraBody);
|
|
598
|
+
if (options?.dropThinkingWhenReasoningEffort) {
|
|
599
|
+
const shaped = params as { reasoning_effort?: unknown; thinking?: unknown };
|
|
600
|
+
if (shaped.reasoning_effort !== undefined) {
|
|
601
|
+
delete shaped.thinking;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Chat Completions streaming request body shaped by the OpenAI-family providers.
|
|
608
|
+
* Extends the vendored SDK params with the compat dialect fields pi-ai emits
|
|
609
|
+
* (binary `thinking`, Qwen `enable_thinking`/`chat_template_kwargs`, nested
|
|
610
|
+
* `reasoning`, gateway `provider`/`providerOptions`, sampling extras). Lives in
|
|
611
|
+
* the shared module beside the request-shaping helpers that mutate it.
|
|
612
|
+
*/
|
|
613
|
+
export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming, "reasoning_effort" | "service_tier"> & {
|
|
614
|
+
top_k?: number;
|
|
615
|
+
min_p?: number;
|
|
616
|
+
repetition_penalty?: number;
|
|
617
|
+
thinking?: { type: "enabled" | "disabled"; keep?: "all" };
|
|
618
|
+
enable_thinking?: boolean;
|
|
619
|
+
preserve_thinking?: boolean;
|
|
620
|
+
chat_template_kwargs?: { enable_thinking?: boolean; preserve_thinking?: boolean };
|
|
621
|
+
reasoning?: { effort?: string } | { enabled: false };
|
|
622
|
+
reasoning_effort?: string | null;
|
|
623
|
+
service_tier?: ServiceTier;
|
|
624
|
+
tool_stream?: boolean;
|
|
625
|
+
provider?: OpenAICompat["openRouterRouting"];
|
|
626
|
+
providerOptions?: { gateway?: { only?: string[]; order?: string[] } };
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/** Reasoning-relevant slice of caller options the Chat Completions dialect dispatch reads. */
|
|
630
|
+
export interface ChatCompletionsReasoningOptions {
|
|
631
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
632
|
+
disableReasoning?: boolean;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export type OpenAICompatEndpoint = "chat-completions" | "responses";
|
|
636
|
+
|
|
637
|
+
export type OpenAIReasoningDisableReason = "caller" | "forced-tool-choice" | "tool-choice" | "not-requested";
|
|
638
|
+
|
|
639
|
+
export type OpenAICompatPolicyCompat = ResolvedOpenAISharedCompat &
|
|
640
|
+
Partial<ResolvedOpenAICompat> &
|
|
641
|
+
Partial<ResolvedOpenAIResponsesCompat>;
|
|
642
|
+
|
|
643
|
+
export interface ResolveOpenAICompatPolicyOptions {
|
|
644
|
+
endpoint: OpenAICompatEndpoint;
|
|
645
|
+
compat?: OpenAICompatPolicyCompat;
|
|
646
|
+
reasoning?: string;
|
|
647
|
+
disableReasoning?: boolean;
|
|
648
|
+
toolChoice?: unknown;
|
|
649
|
+
strictResponsesPairing?: boolean;
|
|
650
|
+
includeEncryptedReasoning?: boolean;
|
|
651
|
+
filterReasoningHistory?: boolean;
|
|
652
|
+
omitReasoningEffort?: boolean;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export interface OpenAICompatPolicy {
|
|
656
|
+
endpoint: OpenAICompatEndpoint;
|
|
657
|
+
compat: OpenAICompatPolicyCompat;
|
|
658
|
+
reasoning: {
|
|
659
|
+
modelSupported: boolean;
|
|
660
|
+
supportsParams: boolean;
|
|
661
|
+
requestedEffort?: string;
|
|
662
|
+
wireEffort?: string;
|
|
663
|
+
enabled: boolean;
|
|
664
|
+
disabled: boolean;
|
|
665
|
+
disableReason?: OpenAIReasoningDisableReason;
|
|
666
|
+
dialect: ResolvedOpenAISharedCompat["thinkingFormat"];
|
|
667
|
+
disableMode: OpenAIReasoningDisableMode;
|
|
668
|
+
omitReasoningEffort: boolean;
|
|
669
|
+
includeEncryptedReasoning: boolean;
|
|
670
|
+
filterReasoningHistory: boolean;
|
|
671
|
+
requiresReasoningContentForToolCalls: boolean;
|
|
672
|
+
requiresReasoningContentForAllAssistantTurns: boolean;
|
|
673
|
+
allowsSyntheticReasoningContentForToolCalls: boolean;
|
|
674
|
+
reasoningContentField?: OpenAICompat["reasoningContentField"];
|
|
675
|
+
requiresThinkingAsText: boolean;
|
|
676
|
+
};
|
|
677
|
+
tools: {
|
|
678
|
+
strictResponsesPairing: boolean;
|
|
679
|
+
toolCallIdKind: "default" | "openai-40" | "mistral-9-alnum";
|
|
680
|
+
};
|
|
681
|
+
messages: {
|
|
682
|
+
systemRole: "system" | "developer";
|
|
683
|
+
supportsDeveloperRole: boolean;
|
|
684
|
+
supportsMultipleSystemMessages: boolean;
|
|
685
|
+
};
|
|
686
|
+
stream: {
|
|
687
|
+
stripSpecialTokens: "deepseek" | false;
|
|
688
|
+
markupHealingPattern?: OpenAIStreamMarkupHealingPattern;
|
|
689
|
+
reasoningDeltasMayBeCumulative: boolean;
|
|
690
|
+
emptyLengthFinishIsContextError: boolean;
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function mapOpenAIReasoningEffort(
|
|
695
|
+
model: Pick<Model, "thinking">,
|
|
696
|
+
compat: OpenAICompatPolicyCompat,
|
|
697
|
+
effort: string,
|
|
698
|
+
): string {
|
|
699
|
+
const level = effort as Effort;
|
|
700
|
+
return compat.reasoningEffortMap?.[level] ?? model.thinking?.effortMap?.[level] ?? effort;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function isImplicitDisableWhenNotRequested(disableMode: OpenAIReasoningDisableMode): boolean {
|
|
704
|
+
return (
|
|
705
|
+
disableMode === "zai-thinking-disabled" ||
|
|
706
|
+
disableMode === "qwen-enable-thinking-false" ||
|
|
707
|
+
disableMode === "qwen-template-false"
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function resolveOpenAICompatPolicy<TApi extends Api>(
|
|
712
|
+
model: Model<TApi>,
|
|
713
|
+
options: ResolveOpenAICompatPolicyOptions,
|
|
714
|
+
): OpenAICompatPolicy {
|
|
715
|
+
const baseCompat = (options.compat ?? model.compat) as OpenAICompatPolicyCompat;
|
|
716
|
+
const requestedEffort = options.reasoning;
|
|
717
|
+
const modelSupported = Boolean(model.reasoning);
|
|
718
|
+
const forcedToolChoiceSuppressesReasoning =
|
|
719
|
+
baseCompat.disableReasoningOnForcedToolChoice &&
|
|
720
|
+
baseCompat.supportsForcedToolChoice &&
|
|
721
|
+
isForcedToolChoice(options.toolChoice);
|
|
722
|
+
const anyToolChoiceSuppressesReasoning =
|
|
723
|
+
!forcedToolChoiceSuppressesReasoning &&
|
|
724
|
+
baseCompat.disableReasoningOnToolChoice &&
|
|
725
|
+
options.toolChoice !== undefined;
|
|
726
|
+
const requestedAndAllowed = requestedEffort !== undefined && !options.disableReasoning && modelSupported;
|
|
727
|
+
const conflictDisableReason: OpenAIReasoningDisableReason | undefined = forcedToolChoiceSuppressesReasoning
|
|
728
|
+
? "forced-tool-choice"
|
|
729
|
+
: anyToolChoiceSuppressesReasoning
|
|
730
|
+
? "tool-choice"
|
|
731
|
+
: undefined;
|
|
732
|
+
const disableReason: OpenAIReasoningDisableReason | undefined = options.disableReasoning
|
|
733
|
+
? "caller"
|
|
734
|
+
: conflictDisableReason;
|
|
735
|
+
const enabledBeforeThinkingVariant = requestedAndAllowed && disableReason === undefined;
|
|
736
|
+
const baseWireEffort =
|
|
737
|
+
enabledBeforeThinkingVariant && requestedEffort !== undefined
|
|
738
|
+
? mapOpenAIReasoningEffort(model, baseCompat, requestedEffort)
|
|
739
|
+
: undefined;
|
|
740
|
+
const disabledByNoneEffort =
|
|
741
|
+
enabledBeforeThinkingVariant &&
|
|
742
|
+
baseCompat.reasoningDisableMode === "zai-thinking-disabled" &&
|
|
743
|
+
baseWireEffort === "none";
|
|
744
|
+
const enabled = enabledBeforeThinkingVariant && !disabledByNoneEffort;
|
|
745
|
+
const compat =
|
|
746
|
+
enabled && baseCompat.whenThinking ? (baseCompat.whenThinking as OpenAICompatPolicyCompat) : baseCompat;
|
|
747
|
+
const omitReasoningEffort =
|
|
748
|
+
options.omitReasoningEffort ?? (compat.omitReasoningEffort || !compat.supportsReasoningEffort);
|
|
749
|
+
const disableMode = compat.reasoningDisableMode;
|
|
750
|
+
let wireEffort =
|
|
751
|
+
enabled && requestedEffort !== undefined ? mapOpenAIReasoningEffort(model, compat, requestedEffort) : undefined;
|
|
752
|
+
const disabledWithoutRequest =
|
|
753
|
+
modelSupported &&
|
|
754
|
+
requestedEffort === undefined &&
|
|
755
|
+
!options.disableReasoning &&
|
|
756
|
+
isImplicitDisableWhenNotRequested(disableMode);
|
|
757
|
+
const disabled =
|
|
758
|
+
(modelSupported && disableReason === "caller") ||
|
|
759
|
+
conflictDisableReason !== undefined ||
|
|
760
|
+
(modelSupported && disabledWithoutRequest) ||
|
|
761
|
+
disabledByNoneEffort;
|
|
762
|
+
if (
|
|
763
|
+
disabled &&
|
|
764
|
+
disableReason === "caller" &&
|
|
765
|
+
requestedEffort === undefined &&
|
|
766
|
+
disableMode === "lowest-effort" &&
|
|
767
|
+
compat.supportsReasoningEffort &&
|
|
768
|
+
!omitReasoningEffort
|
|
769
|
+
) {
|
|
770
|
+
const minEffort = getSupportedEfforts(model)[0];
|
|
771
|
+
if (minEffort === undefined) {
|
|
772
|
+
throw new AIError.ConfigurationError(`Model ${model.provider}/${model.id} has no supported reasoning efforts`);
|
|
773
|
+
}
|
|
774
|
+
wireEffort = mapOpenAIReasoningEffort(model, compat, minEffort);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
endpoint: options.endpoint,
|
|
779
|
+
compat,
|
|
780
|
+
reasoning: {
|
|
781
|
+
modelSupported,
|
|
782
|
+
supportsParams: compat.supportsReasoningParams,
|
|
783
|
+
requestedEffort,
|
|
784
|
+
wireEffort,
|
|
785
|
+
enabled,
|
|
786
|
+
disabled,
|
|
787
|
+
disableReason: disableReason ?? (disabledWithoutRequest || disabledByNoneEffort ? "not-requested" : undefined),
|
|
788
|
+
dialect: compat.thinkingFormat,
|
|
789
|
+
requiresReasoningContentForToolCalls: compat.requiresReasoningContentForToolCalls,
|
|
790
|
+
requiresReasoningContentForAllAssistantTurns: compat.requiresReasoningContentForAllAssistantTurns,
|
|
791
|
+
allowsSyntheticReasoningContentForToolCalls: compat.allowsSyntheticReasoningContentForToolCalls,
|
|
792
|
+
reasoningContentField: compat.reasoningContentField,
|
|
793
|
+
requiresThinkingAsText: compat.requiresThinkingAsText,
|
|
794
|
+
disableMode,
|
|
795
|
+
omitReasoningEffort,
|
|
796
|
+
includeEncryptedReasoning: options.includeEncryptedReasoning ?? compat.includeEncryptedReasoning,
|
|
797
|
+
filterReasoningHistory: options.filterReasoningHistory ?? compat.filterReasoningHistory,
|
|
798
|
+
},
|
|
799
|
+
tools: {
|
|
800
|
+
strictResponsesPairing: options.strictResponsesPairing ?? compat.strictResponsesPairing ?? false,
|
|
801
|
+
toolCallIdKind: compat.requiresMistralToolIds
|
|
802
|
+
? "mistral-9-alnum"
|
|
803
|
+
: compat.usesOpenAIToolCallIdLimit
|
|
804
|
+
? "openai-40"
|
|
805
|
+
: "default",
|
|
806
|
+
},
|
|
807
|
+
messages: {
|
|
808
|
+
systemRole: modelSupported && compat.supportsDeveloperRole ? "developer" : "system",
|
|
809
|
+
supportsDeveloperRole: compat.supportsDeveloperRole,
|
|
810
|
+
supportsMultipleSystemMessages: compat.supportsMultipleSystemMessages ?? true,
|
|
811
|
+
},
|
|
812
|
+
stream: {
|
|
813
|
+
stripSpecialTokens: compat.stripDeepseekSpecialTokens ? "deepseek" : false,
|
|
814
|
+
markupHealingPattern: compat.streamMarkupHealingPattern,
|
|
815
|
+
reasoningDeltasMayBeCumulative: compat.reasoningDeltasMayBeCumulative,
|
|
816
|
+
emptyLengthFinishIsContextError: compat.emptyLengthFinishIsContextError,
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function encodeChatCompletionsDisabledReasoning(
|
|
822
|
+
params: OpenAICompletionsParams,
|
|
823
|
+
disableMode: OpenAIReasoningDisableMode,
|
|
824
|
+
): void {
|
|
825
|
+
delete params.reasoning_effort;
|
|
826
|
+
switch (disableMode) {
|
|
827
|
+
case "zai-thinking-disabled":
|
|
828
|
+
params.thinking = { type: "disabled" };
|
|
829
|
+
break;
|
|
830
|
+
case "qwen-enable-thinking-false":
|
|
831
|
+
params.enable_thinking = false;
|
|
832
|
+
break;
|
|
833
|
+
case "qwen-template-false":
|
|
834
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: false };
|
|
835
|
+
break;
|
|
836
|
+
case "openrouter-enabled-false":
|
|
837
|
+
(params as typeof params & { reasoning?: { effort?: string } | { enabled: false } }).reasoning = {
|
|
838
|
+
enabled: false,
|
|
839
|
+
};
|
|
840
|
+
break;
|
|
841
|
+
default:
|
|
842
|
+
delete params.reasoning;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function applyChatCompletionsCompatPolicy(params: OpenAICompletionsParams, policy: OpenAICompatPolicy): void {
|
|
848
|
+
// `preserve_thinking` is a chat-template HISTORY knob, not a per-turn
|
|
849
|
+
// thinking switch — it controls whether OLDER assistant turns render
|
|
850
|
+
// with `<think>...</think>` on Qwen3.6+. Emit it BEFORE the reasoning
|
|
851
|
+
// state branches and EVERY early-return below, because the wire shape
|
|
852
|
+
// must carry the kwarg in three cases the auto-detected
|
|
853
|
+
// `qwenPreserveThinking` flag covers but `reasoning.enabled` does not:
|
|
854
|
+
//
|
|
855
|
+
// 1. Discovered local Qwen models. `discoverOpenAICompatibleModels`
|
|
856
|
+
// stamps `reasoning: false` on every spec built from a generic
|
|
857
|
+
// `/v1/models` endpoint (the upstream doesn't advertise the
|
|
858
|
+
// capability), so `model.reasoning === false` → `reasoning.enabled
|
|
859
|
+
// === false`, the body wouldn't otherwise see the kwarg, and the
|
|
860
|
+
// encoder's `replayReasoningContent` branch would keep shipping
|
|
861
|
+
// `reasoning_content` only for the template to strip `<think>` from
|
|
862
|
+
// older turns anyway. Exactly the #3528 / #3541 symptom on every
|
|
863
|
+
// discovered Qwen build.
|
|
864
|
+
// 2. Caller-disabled reasoning. The slot's KV cache still holds prior
|
|
865
|
+
// `<think>...</think>` tokens from earlier thinking turns; the
|
|
866
|
+
// template must keep rendering them or cache invalidates at the
|
|
867
|
+
// first historic `<think>`.
|
|
868
|
+
// 3. Forced-tool-choice / DeepSeek-style auto-disable. Same reasoning
|
|
869
|
+
// as (2) — historic thinking blocks have to survive history replay
|
|
870
|
+
// even when the current turn cannot think.
|
|
871
|
+
//
|
|
872
|
+
// Non-Qwen templates ignore the parameter (jinja `is defined` check
|
|
873
|
+
// silently no-ops), so emitting it unconditionally for the Qwen-family
|
|
874
|
+
// + local-cache compat flag is safe.
|
|
875
|
+
if (policy.compat.qwenPreserveThinking) {
|
|
876
|
+
// Mirror the dialect split that gates `enable_thinking`. The
|
|
877
|
+
// `qwen` dialect rides the top-level field (the only place
|
|
878
|
+
// llama.cpp's `--jinja` hook AND Alibaba Cloud Model Studio's
|
|
879
|
+
// compatible-mode look) while the `qwen-chat-template` dialect
|
|
880
|
+
// (NVIDIA NIM, vLLM/SGLang's chat-template-kwargs path) MUST
|
|
881
|
+
// ride only the kwargs copy — NIM's request schema is
|
|
882
|
+
// `additionalProperties: false` and rejects every unknown
|
|
883
|
+
// top-level field, the very reason `enable_thinking` is
|
|
884
|
+
// route-split this way (#2299, see `catalog/src/compat/openai.ts`
|
|
885
|
+
// thinkingFormat comment).
|
|
886
|
+
if (policy.compat.thinkingFormat === "qwen") {
|
|
887
|
+
params.preserve_thinking = true;
|
|
888
|
+
}
|
|
889
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, preserve_thinking: true };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const reasoning = policy.reasoning;
|
|
893
|
+
if ((!reasoning.modelSupported && !reasoning.disabled) || !reasoning.supportsParams) return;
|
|
894
|
+
if (reasoning.enabled) {
|
|
895
|
+
switch (reasoning.disableMode) {
|
|
896
|
+
case "zai-thinking-disabled":
|
|
897
|
+
if (reasoning.wireEffort === "none") {
|
|
898
|
+
encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
params.thinking = { type: "enabled" };
|
|
902
|
+
if (policy.compat.thinkingKeep) params.thinking.keep = policy.compat.thinkingKeep;
|
|
903
|
+
if (policy.compat.supportsReasoningEffort && reasoning.wireEffort !== undefined) {
|
|
904
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
case "qwen-enable-thinking-false":
|
|
908
|
+
params.enable_thinking = true;
|
|
909
|
+
break;
|
|
910
|
+
case "qwen-template-false":
|
|
911
|
+
// Spread so the `preserve_thinking` kwarg hoisted above
|
|
912
|
+
// survives the merge — a bare `{ enable_thinking: true }`
|
|
913
|
+
// would clobber it.
|
|
914
|
+
params.chat_template_kwargs = { ...params.chat_template_kwargs, enable_thinking: true };
|
|
915
|
+
break;
|
|
916
|
+
case "openrouter-enabled-false":
|
|
917
|
+
if (reasoning.wireEffort !== undefined) {
|
|
918
|
+
(params as typeof params & { reasoning?: { effort?: string } }).reasoning = {
|
|
919
|
+
effort: reasoning.wireEffort,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
break;
|
|
923
|
+
default:
|
|
924
|
+
if (!reasoning.omitReasoningEffort && reasoning.wireEffort !== undefined) {
|
|
925
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
926
|
+
}
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (!reasoning.disabled) return;
|
|
932
|
+
if (
|
|
933
|
+
reasoning.disableReason === "caller" &&
|
|
934
|
+
reasoning.requestedEffort === undefined &&
|
|
935
|
+
reasoning.disableMode === "lowest-effort" &&
|
|
936
|
+
reasoning.wireEffort !== undefined
|
|
937
|
+
) {
|
|
938
|
+
params.reasoning_effort = reasoning.wireEffort as Effort;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
encodeChatCompletionsDisabledReasoning(params, reasoning.disableMode);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function applyChatCompletionsReasoningParams(
|
|
945
|
+
params: OpenAICompletionsParams,
|
|
946
|
+
model: Model<"openai-completions">,
|
|
947
|
+
compat: ResolvedOpenAICompat,
|
|
948
|
+
options: (ChatCompletionsReasoningOptions & { toolChoice?: unknown }) | undefined,
|
|
949
|
+
): void {
|
|
950
|
+
applyChatCompletionsCompatPolicy(
|
|
951
|
+
params,
|
|
952
|
+
resolveOpenAICompatPolicy(model, {
|
|
953
|
+
endpoint: "chat-completions",
|
|
954
|
+
compat,
|
|
955
|
+
reasoning: options?.reasoning,
|
|
956
|
+
disableReasoning: options?.disableReasoning,
|
|
957
|
+
toolChoice: options?.toolChoice,
|
|
958
|
+
}),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function disableChatCompletionsReasoningForDialect(
|
|
963
|
+
params: OpenAICompletionsParams,
|
|
964
|
+
compat: ResolvedOpenAICompat,
|
|
965
|
+
): void {
|
|
966
|
+
encodeChatCompletionsDisabledReasoning(params, compat.reasoningDisableMode);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Z.AI/GLM-5.2 reasoning-effort dialect predicate. GLM-5.2 models served on a
|
|
971
|
+
* Z.AI-format host (thinkingFormat "zai") accept `reasoning_effort`, stream tool
|
|
972
|
+
* calls via `tool_stream`, and clamp output to the model cap. Moonshot Kimi and
|
|
973
|
+
* Xiaomi MiMo also resolve to thinkingFormat "zai" with supportsReasoningEffort
|
|
974
|
+
* true but are NOT GLM-5.2, so the model-id check is load-bearing — never swap it
|
|
975
|
+
* for `compat.supportsReasoningEffort`.
|
|
976
|
+
*/
|
|
977
|
+
function isZaiReasoningEffortDialect(model: Model<"openai-completions">, compat: ResolvedOpenAICompat): boolean {
|
|
978
|
+
return compat.thinkingFormat === "zai" && isGlm52ReasoningEffortModelId(model.id);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Output-token clamp for the Z.AI/GLM-5.2 reasoning dialect: these hosts accept
|
|
983
|
+
* the full model window on reasoning turns, so clamp to the model cap. Returns
|
|
984
|
+
* `undefined` for every other model, leaving {@link resolveOpenAIOutputTokenParam}
|
|
985
|
+
* on its default `OPENAI_MAX_OUTPUT_TOKENS` clamp.
|
|
986
|
+
*/
|
|
987
|
+
export function resolveZaiReasoningOutputClamp(
|
|
988
|
+
model: Model<"openai-completions">,
|
|
989
|
+
compat: ResolvedOpenAICompat,
|
|
990
|
+
): number | undefined {
|
|
991
|
+
return isZaiReasoningEffortDialect(model, compat) ? (model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS) : undefined;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
|
|
996
|
+
* (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
|
|
997
|
+
*/
|
|
998
|
+
export function applyChatCompletionsToolStream(
|
|
999
|
+
params: OpenAICompletionsParams,
|
|
1000
|
+
model: Model<"openai-completions">,
|
|
1001
|
+
compat: ResolvedOpenAICompat,
|
|
1002
|
+
): void {
|
|
1003
|
+
if (
|
|
1004
|
+
isZaiReasoningEffortDialect(model, compat) &&
|
|
1005
|
+
compat.supportsReasoningEffort &&
|
|
1006
|
+
Array.isArray(params.tools) &&
|
|
1007
|
+
params.tools.length > 0
|
|
1008
|
+
) {
|
|
1009
|
+
params.tool_stream = true;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export function isCompiledGrammarTooLargeStrictError(
|
|
1014
|
+
error: unknown,
|
|
1015
|
+
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1016
|
+
): boolean {
|
|
1017
|
+
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
|
1018
|
+
if (status !== 400) return false;
|
|
1019
|
+
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1020
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1021
|
+
.join("\n");
|
|
1022
|
+
return (
|
|
1023
|
+
/invalid_request_error/i.test(messageParts) &&
|
|
1024
|
+
/compiled grammar/i.test(messageParts) &&
|
|
1025
|
+
/too large/i.test(messageParts)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function shouldRetryWithoutStrictTools(
|
|
1030
|
+
error: unknown,
|
|
1031
|
+
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1032
|
+
strictToolsApplied: boolean,
|
|
1033
|
+
tools: Tool[] | undefined,
|
|
1034
|
+
): boolean {
|
|
1035
|
+
if (!tools || tools.length === 0 || !strictToolsApplied) return false;
|
|
1036
|
+
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
|
1037
|
+
if (status !== 400 && status !== 422) return false;
|
|
1038
|
+
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1039
|
+
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1040
|
+
.join("\n");
|
|
1041
|
+
return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function/i.test(
|
|
1042
|
+
messageParts,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
|
|
1047
|
+
if (!value || value.length === 0) return undefined;
|
|
1048
|
+
const wellFormed = value.toWellFormed();
|
|
1049
|
+
if (wellFormed.length <= maxLength) return wellFormed;
|
|
1050
|
+
return `${hashPrefix}${Bun.hash(wellFormed).toString(36)}`;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
1054
|
+
"response.created",
|
|
1055
|
+
"response.output_item.added",
|
|
1056
|
+
"response.reasoning_summary_part.added",
|
|
1057
|
+
"response.reasoning_summary_text.delta",
|
|
1058
|
+
"response.reasoning_summary_part.done",
|
|
1059
|
+
"response.reasoning_text.delta",
|
|
1060
|
+
"response.content_part.added",
|
|
1061
|
+
"response.output_text.delta",
|
|
1062
|
+
"response.refusal.delta",
|
|
1063
|
+
"response.function_call_arguments.delta",
|
|
1064
|
+
"response.function_call_arguments.done",
|
|
1065
|
+
"response.custom_tool_call_input.delta",
|
|
1066
|
+
"response.custom_tool_call_input.done",
|
|
1067
|
+
"response.output_item.done",
|
|
1068
|
+
"response.completed",
|
|
1069
|
+
"response.incomplete",
|
|
1070
|
+
"response.failed",
|
|
1071
|
+
"error",
|
|
1072
|
+
]);
|
|
1073
|
+
|
|
1074
|
+
export function isOpenAIResponsesProgressEvent(event: unknown): boolean {
|
|
1075
|
+
if (!event || typeof event !== "object") return false;
|
|
1076
|
+
const type = (event as { type?: unknown }).type;
|
|
1077
|
+
return typeof type === "string" && OPENAI_RESPONSES_PROGRESS_EVENT_TYPES.has(type);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
export function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string {
|
|
1081
|
+
const payload: TextSignatureV1 = { v: 1, id };
|
|
1082
|
+
if (phase) payload.phase = phase;
|
|
1083
|
+
return JSON.stringify(payload);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export function parseTextSignature(
|
|
1087
|
+
signature: string | undefined,
|
|
1088
|
+
): { id: string; phase?: TextSignatureV1["phase"] } | undefined {
|
|
1089
|
+
if (!signature) return undefined;
|
|
1090
|
+
if (signature.startsWith("{")) {
|
|
1091
|
+
try {
|
|
1092
|
+
const parsed = JSON.parse(signature) as Partial<TextSignatureV1>;
|
|
1093
|
+
if (parsed.v === 1 && typeof parsed.id === "string") {
|
|
1094
|
+
if (parsed.phase === "commentary" || parsed.phase === "final_answer") {
|
|
1095
|
+
return { id: parsed.id, phase: parsed.phase };
|
|
1096
|
+
}
|
|
1097
|
+
return { id: parsed.id };
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
// Fall through to legacy plain-string handling.
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return { id: signature };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function encodeResponsesToolCallId(callId: string, itemId: string | null | undefined): string {
|
|
1107
|
+
const stableItemId = itemId && itemId.length > 0 ? itemId : `fc_${Bun.hash(callId).toString(36)}`;
|
|
1108
|
+
return `${callId}|${stableItemId}`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function normalizeResponsesToolCallIdForTransform(
|
|
1112
|
+
id: string,
|
|
1113
|
+
model?: Model<Api>,
|
|
1114
|
+
source?: AssistantMessage,
|
|
1115
|
+
): string {
|
|
1116
|
+
if (!id.includes("|")) return id;
|
|
1117
|
+
const isForeignToolCall =
|
|
1118
|
+
source != null && model != null && (source.provider !== model.provider || source.api !== model.api);
|
|
1119
|
+
if (isForeignToolCall) {
|
|
1120
|
+
const [callId, itemId] = id.split("|");
|
|
1121
|
+
const normalizeIdPart = (part: string): string => {
|
|
1122
|
+
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1123
|
+
const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
1124
|
+
return truncated.replace(/_+$/, "");
|
|
1125
|
+
};
|
|
1126
|
+
const normalizedCallId = normalizeIdPart(callId);
|
|
1127
|
+
let normalizedItemId = `fc_${Bun.hash(itemId).toString(36)}`;
|
|
1128
|
+
if (normalizedItemId.length > 64) normalizedItemId = normalizedItemId.slice(0, 64);
|
|
1129
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
1130
|
+
}
|
|
1131
|
+
const normalized = normalizeResponsesToolCallId(id);
|
|
1132
|
+
return `${normalized.callId}|${normalized.itemId}`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function collectKnownCallIds(messages: ResponseInput): Set<string> {
|
|
1136
|
+
const knownCallIds = new Set<string>();
|
|
1137
|
+
for (const item of messages) {
|
|
1138
|
+
if (item.type === "function_call" && typeof item.call_id === "string") {
|
|
1139
|
+
knownCallIds.add(item.call_id);
|
|
1140
|
+
} else if (
|
|
1141
|
+
(item as { type?: string }).type === "custom_tool_call" &&
|
|
1142
|
+
typeof (item as { call_id?: string }).call_id === "string"
|
|
1143
|
+
) {
|
|
1144
|
+
knownCallIds.add((item as { call_id: string }).call_id);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return knownCallIds;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Scan replay items for call_ids that were originally custom tool calls. */
|
|
1151
|
+
export function collectCustomCallIds(messages: ResponseInput): Set<string> {
|
|
1152
|
+
const customCallIds = new Set<string>();
|
|
1153
|
+
for (const item of messages) {
|
|
1154
|
+
if (
|
|
1155
|
+
(item as { type?: string }).type === "custom_tool_call" &&
|
|
1156
|
+
typeof (item as { call_id?: string }).call_id === "string"
|
|
1157
|
+
) {
|
|
1158
|
+
customCallIds.add((item as { call_id: string }).call_id);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return customCallIds;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Convert orphan `function_call_output` / `custom_tool_call_output` items —
|
|
1166
|
+
* those whose `call_id` has no matching preceding `function_call` /
|
|
1167
|
+
* `custom_tool_call` in the same input — into assistant text notes.
|
|
1168
|
+
*
|
|
1169
|
+
* The Responses API rejects unpaired outputs with
|
|
1170
|
+
* `400 No tool call found for function call output with call_id …`. Orphans
|
|
1171
|
+
* sneak in through two paths today:
|
|
1172
|
+
*
|
|
1173
|
+
* - A previous turn's `providerPayload` snapshot replaces the input array via
|
|
1174
|
+
* the `dt: false` splice (see {@link convertConversationMessages}), wiping
|
|
1175
|
+
* the matching `function_call` while leaving the matching
|
|
1176
|
+
* `function_call_output` queued in a later `toolResult`.
|
|
1177
|
+
* - A locally-rejected tool call (argument-validation failure, hook reject,
|
|
1178
|
+
* aborted turn before the call streamed) produces a tool result without a
|
|
1179
|
+
* `function_call` ever landing in any persisted provider payload.
|
|
1180
|
+
*
|
|
1181
|
+
* Dropping the result loses information the model needs to recover; sending
|
|
1182
|
+
* it as-is 400s the request. Folding it into an assistant `message` preserves
|
|
1183
|
+
* the payload (call_id + truncated output) while staying within the Responses
|
|
1184
|
+
* input grammar. Matches the behavior of {@link transformRequestBody} in the
|
|
1185
|
+
* codex provider — issue #1351 / regression of #472.
|
|
1186
|
+
*/
|
|
1187
|
+
export function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput {
|
|
1188
|
+
const knownCallIds = new Set<string>();
|
|
1189
|
+
for (const item of input) {
|
|
1190
|
+
const t = (item as { type?: string }).type;
|
|
1191
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
1192
|
+
if (typeof callId !== "string") continue;
|
|
1193
|
+
if (t === "function_call" || t === "custom_tool_call") knownCallIds.add(callId);
|
|
1194
|
+
}
|
|
1195
|
+
let hasOrphan = false;
|
|
1196
|
+
for (const item of input) {
|
|
1197
|
+
const t = (item as { type?: string }).type;
|
|
1198
|
+
if (t !== "function_call_output" && t !== "custom_tool_call_output") continue;
|
|
1199
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
1200
|
+
if (typeof callId === "string" && !knownCallIds.has(callId)) {
|
|
1201
|
+
hasOrphan = true;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (!hasOrphan) return input;
|
|
1206
|
+
return input.map(item => {
|
|
1207
|
+
const t = (item as { type?: string }).type;
|
|
1208
|
+
if (t !== "function_call_output" && t !== "custom_tool_call_output") return item;
|
|
1209
|
+
const record = item as { call_id?: unknown; output?: unknown; name?: unknown };
|
|
1210
|
+
const callId = record.call_id;
|
|
1211
|
+
if (typeof callId !== "string" || knownCallIds.has(callId)) return item;
|
|
1212
|
+
const toolName = typeof record.name === "string" && record.name.length > 0 ? record.name : "tool";
|
|
1213
|
+
const rawOutput = record.output;
|
|
1214
|
+
let text: string;
|
|
1215
|
+
if (typeof rawOutput === "string") text = rawOutput;
|
|
1216
|
+
else if (rawOutput == null) text = "";
|
|
1217
|
+
else {
|
|
1218
|
+
try {
|
|
1219
|
+
text = JSON.stringify(rawOutput);
|
|
1220
|
+
} catch {
|
|
1221
|
+
text = String(rawOutput);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const ORPHAN_OUTPUT_LIMIT = 16_000;
|
|
1225
|
+
if (text.length > ORPHAN_OUTPUT_LIMIT) text = `${text.slice(0, ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
1226
|
+
return {
|
|
1227
|
+
type: "message",
|
|
1228
|
+
role: "assistant",
|
|
1229
|
+
content: `[Orphan ${toolName} result; call_id=${callId}]: ${text}`,
|
|
1230
|
+
} as ResponseInput[number];
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** Placeholder output for a tool call whose result is absent from the input. */
|
|
1235
|
+
const ORPHAN_TOOL_CALL_PLACEHOLDER =
|
|
1236
|
+
"[No tool output recorded: the tool call was interrupted before it produced a result.]";
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
|
|
1240
|
+
* for every `function_call` / `custom_tool_call` whose `call_id` has no matching
|
|
1241
|
+
* output later in the same input. The Responses API rejects an unpaired call
|
|
1242
|
+
* with `400 No tool output found for function call …`.
|
|
1243
|
+
*
|
|
1244
|
+
* Orphan calls surface when the user branches/navigates the session tree to a
|
|
1245
|
+
* node that ends on a tool call (the tool-result child is excluded from the
|
|
1246
|
+
* reconstructed history) or when a turn is aborted/crashes after the call
|
|
1247
|
+
* streamed but before its result persisted. Dropping the call would erase the
|
|
1248
|
+
* assistant's action; a placeholder output keeps the call visible so the model
|
|
1249
|
+
* can recover (e.g. re-issue the call). Symmetric to
|
|
1250
|
+
* {@link repairOrphanResponsesToolOutputs}.
|
|
1251
|
+
*/
|
|
1252
|
+
export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput {
|
|
1253
|
+
const outputCallIds = new Set<string>();
|
|
1254
|
+
for (const item of input) {
|
|
1255
|
+
const t = (item as { type?: string }).type;
|
|
1256
|
+
if (t !== "function_call_output" && t !== "custom_tool_call_output") continue;
|
|
1257
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
1258
|
+
if (typeof callId === "string") outputCallIds.add(callId);
|
|
1259
|
+
}
|
|
1260
|
+
let hasOrphan = false;
|
|
1261
|
+
for (const item of input) {
|
|
1262
|
+
const t = (item as { type?: string }).type;
|
|
1263
|
+
if (t !== "function_call" && t !== "custom_tool_call") continue;
|
|
1264
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
1265
|
+
if (typeof callId === "string" && !outputCallIds.has(callId)) {
|
|
1266
|
+
hasOrphan = true;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (!hasOrphan) return input;
|
|
1271
|
+
const repaired: ResponseInput = [];
|
|
1272
|
+
for (const item of input) {
|
|
1273
|
+
repaired.push(item);
|
|
1274
|
+
const t = (item as { type?: string }).type;
|
|
1275
|
+
if (t !== "function_call" && t !== "custom_tool_call") continue;
|
|
1276
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
1277
|
+
if (typeof callId !== "string" || outputCallIds.has(callId)) continue;
|
|
1278
|
+
repaired.push({
|
|
1279
|
+
type: t === "custom_tool_call" ? "custom_tool_call_output" : "function_call_output",
|
|
1280
|
+
call_id: callId,
|
|
1281
|
+
output: ORPHAN_TOOL_CALL_PLACEHOLDER,
|
|
1282
|
+
} as ResponseInput[number]);
|
|
1283
|
+
}
|
|
1284
|
+
return repaired;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Some Responses backends (notably GitHub Copilot) reject the OpenAI image
|
|
1289
|
+
* `detail: "original"` value with a 400. When the model does not advertise
|
|
1290
|
+
* support for it, degrade `"original"` to `"auto"` so the request still goes
|
|
1291
|
+
* through with the closest valid fidelity instead of failing outright. See #2822.
|
|
1292
|
+
*/
|
|
1293
|
+
function clampResponsesImageDetail(
|
|
1294
|
+
detail: ImageContent["detail"],
|
|
1295
|
+
supportsImageDetailOriginal: boolean,
|
|
1296
|
+
): ResponseInputImage["detail"] {
|
|
1297
|
+
const resolved = detail ?? "auto";
|
|
1298
|
+
return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export function convertResponsesInputContent(
|
|
1302
|
+
content: string | Array<TextContent | ImageContent>,
|
|
1303
|
+
supportsImages: boolean,
|
|
1304
|
+
supportsImageDetailOriginal: boolean,
|
|
1305
|
+
): ResponseInputContent[] | undefined {
|
|
1306
|
+
if (typeof content === "string") {
|
|
1307
|
+
if (content.trim().length === 0) return undefined;
|
|
1308
|
+
return [{ type: "input_text", text: content.toWellFormed() } satisfies ResponseInputText];
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const { textBlocks, imageBlocks, omittedImages } = partitionVisionContent(content, supportsImages);
|
|
1312
|
+
const normalizedContent: ResponseInputContent[] = [];
|
|
1313
|
+
for (const item of textBlocks) {
|
|
1314
|
+
const text = item.text.toWellFormed();
|
|
1315
|
+
if (text.trim().length === 0) continue;
|
|
1316
|
+
normalizedContent.push({
|
|
1317
|
+
type: "input_text",
|
|
1318
|
+
text,
|
|
1319
|
+
} satisfies ResponseInputText);
|
|
1320
|
+
}
|
|
1321
|
+
for (const item of imageBlocks) {
|
|
1322
|
+
normalizedContent.push({
|
|
1323
|
+
type: "input_image",
|
|
1324
|
+
detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
|
|
1325
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
1326
|
+
} satisfies ResponseInputImage);
|
|
1327
|
+
}
|
|
1328
|
+
if (omittedImages) {
|
|
1329
|
+
normalizedContent.push({
|
|
1330
|
+
type: "input_text",
|
|
1331
|
+
text: NON_VISION_IMAGE_PLACEHOLDER,
|
|
1332
|
+
} satisfies ResponseInputText);
|
|
1333
|
+
}
|
|
1334
|
+
return normalizedContent.length > 0 ? normalizedContent : undefined;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
1338
|
+
model: Model<TApi>;
|
|
1339
|
+
context: Context;
|
|
1340
|
+
strictResponsesPairing: boolean;
|
|
1341
|
+
supportsImageDetailOriginal: boolean;
|
|
1342
|
+
systemRole?: "system" | "developer";
|
|
1343
|
+
nativeHistory?: {
|
|
1344
|
+
replay: boolean;
|
|
1345
|
+
filterReasoning: boolean;
|
|
1346
|
+
};
|
|
1347
|
+
includeThinkingSignatures?: boolean;
|
|
1348
|
+
developerStringContent?: boolean;
|
|
1349
|
+
repairOrphanOutputs?: boolean;
|
|
1350
|
+
/** Preserve assistant message item IDs from text signatures during fallback replay. */
|
|
1351
|
+
preserveAssistantMessageIds?: boolean;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput {
|
|
1355
|
+
const messages: ResponseInput = [];
|
|
1356
|
+
const systemPrompts = options.systemRole ? normalizeSystemPrompts(options.context.systemPrompt) : [];
|
|
1357
|
+
for (const systemPrompt of systemPrompts) {
|
|
1358
|
+
messages.push({ role: options.systemRole as "system" | "developer", content: systemPrompt });
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
let knownCallIds = new Set<string>();
|
|
1362
|
+
const customCallIds = new Set<string>();
|
|
1363
|
+
const transformedMessages = transformMessages(
|
|
1364
|
+
options.context.messages,
|
|
1365
|
+
options.model,
|
|
1366
|
+
normalizeResponsesToolCallIdForTransform,
|
|
1367
|
+
);
|
|
1368
|
+
const filterReasoning = <T extends { type?: string }>(items: T[]): T[] =>
|
|
1369
|
+
options.nativeHistory?.filterReasoning ? items.filter(item => item?.type !== "reasoning") : items;
|
|
1370
|
+
const includeThinkingSignatures = options.includeThinkingSignatures ?? options.nativeHistory?.replay ?? true;
|
|
1371
|
+
|
|
1372
|
+
let msgIndex = 0;
|
|
1373
|
+
for (const msg of transformedMessages) {
|
|
1374
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
1375
|
+
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
1376
|
+
const historyItems = options.nativeHistory
|
|
1377
|
+
? getOpenAIResponsesHistoryItems(providerPayload, options.model.provider)
|
|
1378
|
+
: undefined;
|
|
1379
|
+
const shouldReplayPayloadItems =
|
|
1380
|
+
options.nativeHistory?.replay ||
|
|
1381
|
+
(historyItems?.some(item => {
|
|
1382
|
+
if (!item || typeof item !== "object") return false;
|
|
1383
|
+
const candidate = item as { type?: unknown };
|
|
1384
|
+
return candidate.type === "compaction" || candidate.type === "compaction_summary";
|
|
1385
|
+
}) ??
|
|
1386
|
+
false);
|
|
1387
|
+
if (historyItems && shouldReplayPayloadItems) {
|
|
1388
|
+
messages.push(...sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems)));
|
|
1389
|
+
knownCallIds = collectKnownCallIds(messages);
|
|
1390
|
+
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1391
|
+
msgIndex++;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
const content = convertResponsesInputContent(
|
|
1395
|
+
msg.content,
|
|
1396
|
+
options.model.input.includes("image"),
|
|
1397
|
+
options.supportsImageDetailOriginal,
|
|
1398
|
+
);
|
|
1399
|
+
if (!content) continue;
|
|
1400
|
+
messages.push({
|
|
1401
|
+
role: "user",
|
|
1402
|
+
content:
|
|
1403
|
+
options.developerStringContent && msg.role === "developer" && typeof msg.content === "string"
|
|
1404
|
+
? msg.content.toWellFormed()
|
|
1405
|
+
: content,
|
|
1406
|
+
});
|
|
1407
|
+
} else if (msg.role === "assistant") {
|
|
1408
|
+
const assistantMsg = msg as AssistantMessage;
|
|
1409
|
+
const providerPayload =
|
|
1410
|
+
options.nativeHistory?.replay &&
|
|
1411
|
+
assistantMsg.api === options.model.api &&
|
|
1412
|
+
assistantMsg.model === options.model.id
|
|
1413
|
+
? getOpenAIResponsesHistoryPayload(
|
|
1414
|
+
assistantMsg.providerPayload,
|
|
1415
|
+
options.model.provider,
|
|
1416
|
+
assistantMsg.provider,
|
|
1417
|
+
)
|
|
1418
|
+
: undefined;
|
|
1419
|
+
const historyItems = providerPayload?.items;
|
|
1420
|
+
if (historyItems) {
|
|
1421
|
+
const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems));
|
|
1422
|
+
if (providerPayload?.dt) {
|
|
1423
|
+
messages.push(...sanitizedHistoryItems);
|
|
1424
|
+
} else {
|
|
1425
|
+
messages.splice(0, messages.length, ...sanitizedHistoryItems);
|
|
1426
|
+
}
|
|
1427
|
+
knownCallIds = collectKnownCallIds(messages);
|
|
1428
|
+
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1429
|
+
msgIndex++;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const outputItems = convertResponsesAssistantMessage(
|
|
1434
|
+
assistantMsg,
|
|
1435
|
+
options.model,
|
|
1436
|
+
msgIndex,
|
|
1437
|
+
knownCallIds,
|
|
1438
|
+
includeThinkingSignatures,
|
|
1439
|
+
customCallIds,
|
|
1440
|
+
options.preserveAssistantMessageIds,
|
|
1441
|
+
);
|
|
1442
|
+
if (outputItems.length === 0) continue;
|
|
1443
|
+
messages.push(...outputItems);
|
|
1444
|
+
} else if (msg.role === "toolResult") {
|
|
1445
|
+
appendResponsesToolResultMessages(
|
|
1446
|
+
messages,
|
|
1447
|
+
msg,
|
|
1448
|
+
options.model,
|
|
1449
|
+
options.strictResponsesPairing,
|
|
1450
|
+
options.supportsImageDetailOriginal,
|
|
1451
|
+
knownCallIds,
|
|
1452
|
+
customCallIds,
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
msgIndex++;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const withRepairedOutputs = options.repairOrphanOutputs ? repairOrphanResponsesToolOutputs(messages) : messages;
|
|
1459
|
+
return repairOrphanResponsesToolCalls(withRepairedOutputs);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
|
|
1463
|
+
|
|
1464
|
+
function parseResponseReasoningReplayItem(signature: string | undefined): ResponseReasoningItem | undefined {
|
|
1465
|
+
if (!signature) return undefined;
|
|
1466
|
+
try {
|
|
1467
|
+
const parsed = JSON.parse(signature) as unknown;
|
|
1468
|
+
if (!parsed || typeof parsed !== "object") return undefined;
|
|
1469
|
+
if (!("type" in parsed) || parsed.type !== "reasoning") return undefined;
|
|
1470
|
+
if (!("id" in parsed) || typeof parsed.id !== "string") return undefined;
|
|
1471
|
+
return parsed as ResponseReasoningItem;
|
|
1472
|
+
} catch {
|
|
1473
|
+
return undefined;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
1478
|
+
assistantMsg: AssistantMessage,
|
|
1479
|
+
model: Model<TApi>,
|
|
1480
|
+
msgIndex: number,
|
|
1481
|
+
knownCallIds: Set<string>,
|
|
1482
|
+
includeThinkingSignatures = true,
|
|
1483
|
+
customCallIds?: Set<string>,
|
|
1484
|
+
preserveMessageIds = false,
|
|
1485
|
+
): ResponseInput {
|
|
1486
|
+
const outputItems: ResponseInput = [];
|
|
1487
|
+
let unsignedTextBlocks = 0;
|
|
1488
|
+
const hasReplayableReasoningItem =
|
|
1489
|
+
includeThinkingSignatures &&
|
|
1490
|
+
assistantMsg.stopReason !== "error" &&
|
|
1491
|
+
assistantMsg.content.some(
|
|
1492
|
+
block => block.type === "thinking" && parseResponseReasoningReplayItem(block.thinkingSignature) !== undefined,
|
|
1493
|
+
);
|
|
1494
|
+
const isDifferentModel =
|
|
1495
|
+
assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
1496
|
+
|
|
1497
|
+
for (const block of assistantMsg.content) {
|
|
1498
|
+
if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
|
|
1499
|
+
if (!includeThinkingSignatures) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
|
|
1503
|
+
if (reasoningItem) outputItems.push(reasoningItem);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (block.type === "text") {
|
|
1508
|
+
const parsedSignature = parseTextSignature(block.textSignature);
|
|
1509
|
+
let msgId = parsedSignature?.id;
|
|
1510
|
+
if (!msgId) {
|
|
1511
|
+
if (hasReplayableReasoningItem) {
|
|
1512
|
+
// Distinct ids per unsigned block: several text blocks in one message
|
|
1513
|
+
// (cross-provider replay downgrades thinking → text) must not share an id.
|
|
1514
|
+
msgId = unsignedTextBlocks === 0 ? `msg_${msgIndex}` : `msg_${msgIndex}_${unsignedTextBlocks}`;
|
|
1515
|
+
unsignedTextBlocks += 1;
|
|
1516
|
+
}
|
|
1517
|
+
} else if (!preserveMessageIds && !hasReplayableReasoningItem) {
|
|
1518
|
+
// Without the matching reasoning item the server rejects replayed
|
|
1519
|
+
// item ids (#4173) — drop them regardless of shape, including
|
|
1520
|
+
// legacy plain-string signatures that would otherwise fall into
|
|
1521
|
+
// the >64-char hash branch and fabricate a bogus msg_ id.
|
|
1522
|
+
msgId = undefined;
|
|
1523
|
+
} else if (msgId.length > 64) {
|
|
1524
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
1525
|
+
}
|
|
1526
|
+
const messageItem: ResponsesReplayAssistantMessage = {
|
|
1527
|
+
type: "message",
|
|
1528
|
+
role: "assistant",
|
|
1529
|
+
content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
|
|
1530
|
+
status: "completed",
|
|
1531
|
+
...(msgId ? { id: msgId } : {}),
|
|
1532
|
+
...(parsedSignature?.phase ? { phase: parsedSignature.phase } : {}),
|
|
1533
|
+
};
|
|
1534
|
+
outputItems.push(messageItem as ResponseInput[number]);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (block.type !== "toolCall") {
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
|
|
1543
|
+
let itemId: string | undefined = normalized.itemId;
|
|
1544
|
+
if (
|
|
1545
|
+
!hasReplayableReasoningItem &&
|
|
1546
|
+
(itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
|
|
1547
|
+
) {
|
|
1548
|
+
itemId = undefined;
|
|
1549
|
+
} else if (
|
|
1550
|
+
isDifferentModel &&
|
|
1551
|
+
(itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))
|
|
1552
|
+
) {
|
|
1553
|
+
itemId = undefined;
|
|
1554
|
+
}
|
|
1555
|
+
knownCallIds.add(normalized.callId);
|
|
1556
|
+
if (block.customWireName) {
|
|
1557
|
+
const rawInput = typeof block.arguments?.input === "string" ? block.arguments.input : "";
|
|
1558
|
+
customCallIds?.add(normalized.callId);
|
|
1559
|
+
outputItems.push({
|
|
1560
|
+
type: "custom_tool_call",
|
|
1561
|
+
...(itemId ? { id: itemId } : {}),
|
|
1562
|
+
call_id: normalized.callId,
|
|
1563
|
+
name: block.customWireName,
|
|
1564
|
+
input: rawInput,
|
|
1565
|
+
} as ResponseInput[number]);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
outputItems.push({
|
|
1569
|
+
type: "function_call",
|
|
1570
|
+
...(itemId ? { id: itemId } : {}),
|
|
1571
|
+
call_id: normalized.callId,
|
|
1572
|
+
name: block.name,
|
|
1573
|
+
arguments: JSON.stringify(block.arguments),
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
return outputItems;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
1581
|
+
messages: ResponseInput,
|
|
1582
|
+
toolResult: ToolResultMessage,
|
|
1583
|
+
model: Model<TApi>,
|
|
1584
|
+
strictResponsesPairing: boolean,
|
|
1585
|
+
supportsImageDetailOriginal: boolean,
|
|
1586
|
+
knownCallIds: ReadonlySet<string>,
|
|
1587
|
+
customCallIds?: ReadonlySet<string>,
|
|
1588
|
+
): void {
|
|
1589
|
+
const supportsImages = model.input.includes("image");
|
|
1590
|
+
const textResult = toolResult.content
|
|
1591
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
1592
|
+
.map(block => block.text)
|
|
1593
|
+
.join("\n");
|
|
1594
|
+
const hasImages = toolResult.content.some((block): block is ImageContent => block.type === "image");
|
|
1595
|
+
const omittedImages = hasImages && !supportsImages;
|
|
1596
|
+
const normalized = normalizeResponsesToolCallId(toolResult.toolCallId);
|
|
1597
|
+
const output = (
|
|
1598
|
+
omittedImages
|
|
1599
|
+
? joinTextWithImagePlaceholder(textResult, true)
|
|
1600
|
+
: textResult.length > 0
|
|
1601
|
+
? textResult
|
|
1602
|
+
: "(see attached image)"
|
|
1603
|
+
).toWellFormed();
|
|
1604
|
+
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
|
|
1605
|
+
// Strict backends (Azure, Copilot) reject unpaired outputs outright, but
|
|
1606
|
+
// silently dropping the result loses information the model needs. Fold it
|
|
1607
|
+
// into an assistant note instead (same shape as repairOrphanResponsesToolOutputs).
|
|
1608
|
+
const limit = 16_000;
|
|
1609
|
+
const noteText = output.length > limit ? `${output.slice(0, limit)}\n...[truncated]` : output;
|
|
1610
|
+
messages.push({
|
|
1611
|
+
type: "message",
|
|
1612
|
+
role: "assistant",
|
|
1613
|
+
content: `[Orphan ${toolResult.toolName || "tool"} result; call_id=${normalized.callId}]: ${noteText}`,
|
|
1614
|
+
} as ResponseInput[number]);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (customCallIds?.has(normalized.callId)) {
|
|
1618
|
+
messages.push({
|
|
1619
|
+
type: "custom_tool_call_output",
|
|
1620
|
+
call_id: normalized.callId,
|
|
1621
|
+
output,
|
|
1622
|
+
} as ResponseInput[number]);
|
|
1623
|
+
} else {
|
|
1624
|
+
messages.push({
|
|
1625
|
+
type: "function_call_output",
|
|
1626
|
+
call_id: normalized.callId,
|
|
1627
|
+
output,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (!hasImages || !supportsImages) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const contentParts: ResponseInputContent[] = [
|
|
1636
|
+
{ type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText,
|
|
1637
|
+
];
|
|
1638
|
+
for (const block of toolResult.content) {
|
|
1639
|
+
if (block.type === "image") {
|
|
1640
|
+
contentParts.push({
|
|
1641
|
+
type: "input_image",
|
|
1642
|
+
detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
|
|
1643
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
1644
|
+
} satisfies ResponseInputImage);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
messages.push({ role: "user", content: contentParts });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Per-block accumulation helpers shared by the two Responses decode loops —
|
|
1652
|
+
* {@link processResponsesStream} (generic Responses) and the Codex stream
|
|
1653
|
+
* handler in `openai-codex-responses.ts`. Each endpoint keeps its own
|
|
1654
|
+
* item-routing, terminal handling, and transport bookkeeping; these own only
|
|
1655
|
+
* the leaf mutations on an already-resolved open block, so the
|
|
1656
|
+
* append/parse/finalize logic lives in exactly one place. The caller passes the
|
|
1657
|
+
* `contentIndex` its router resolved (generic uses `output.content.indexOf`;
|
|
1658
|
+
* Codex uses the open item's recorded index) so the emitted stream events match
|
|
1659
|
+
* each decoder's existing behavior byte-for-byte.
|
|
1660
|
+
*/
|
|
1661
|
+
type ResponsesToolCallBlock = ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number };
|
|
1662
|
+
|
|
1663
|
+
export function appendReasoningSummaryPart(
|
|
1664
|
+
item: ResponseReasoningItem,
|
|
1665
|
+
part: ResponseReasoningItem["summary"][number],
|
|
1666
|
+
): void {
|
|
1667
|
+
item.summary = item.summary || [];
|
|
1668
|
+
item.summary.push(part);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
export function appendReasoningSummaryTextDelta(
|
|
1672
|
+
item: ResponseReasoningItem,
|
|
1673
|
+
block: ThinkingContent,
|
|
1674
|
+
delta: string,
|
|
1675
|
+
stream: AssistantMessageEventStream,
|
|
1676
|
+
output: AssistantMessage,
|
|
1677
|
+
contentIndex: number,
|
|
1678
|
+
): void {
|
|
1679
|
+
item.summary = item.summary || [];
|
|
1680
|
+
const lastPart = item.summary[item.summary.length - 1];
|
|
1681
|
+
if (!lastPart) return;
|
|
1682
|
+
block.thinking += delta;
|
|
1683
|
+
lastPart.text += delta;
|
|
1684
|
+
stream.push({ type: "thinking_delta", contentIndex, delta, partial: output });
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
export function appendReasoningSummaryPartDone(
|
|
1688
|
+
item: ResponseReasoningItem,
|
|
1689
|
+
block: ThinkingContent,
|
|
1690
|
+
stream: AssistantMessageEventStream,
|
|
1691
|
+
output: AssistantMessage,
|
|
1692
|
+
contentIndex: number,
|
|
1693
|
+
): void {
|
|
1694
|
+
item.summary = item.summary || [];
|
|
1695
|
+
const lastPart = item.summary[item.summary.length - 1];
|
|
1696
|
+
if (!lastPart) return;
|
|
1697
|
+
block.thinking += "\n\n";
|
|
1698
|
+
lastPart.text += "\n\n";
|
|
1699
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: "\n\n", partial: output });
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
export function appendMessageContentPart(
|
|
1703
|
+
item: ResponseOutputMessage,
|
|
1704
|
+
part: ResponseContentPartAddedEvent["part"] | undefined,
|
|
1705
|
+
): void {
|
|
1706
|
+
item.content = item.content || [];
|
|
1707
|
+
if (part && (part.type === "output_text" || part.type === "refusal")) {
|
|
1708
|
+
item.content.push(part);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
export function appendMessageTextDelta(
|
|
1713
|
+
item: ResponseOutputMessage,
|
|
1714
|
+
block: TextContent,
|
|
1715
|
+
delta: string,
|
|
1716
|
+
stream: AssistantMessageEventStream,
|
|
1717
|
+
output: AssistantMessage,
|
|
1718
|
+
contentIndex: number,
|
|
1719
|
+
partType: "output_text" | "refusal",
|
|
1720
|
+
): void {
|
|
1721
|
+
item.content = item.content || [];
|
|
1722
|
+
let lastPart = item.content[item.content.length - 1];
|
|
1723
|
+
if (lastPart?.type !== partType) {
|
|
1724
|
+
// `content_part.added` never arrived (lossy proxy) — synthesize the part
|
|
1725
|
+
// so live text still streams instead of freezing until output_item.done.
|
|
1726
|
+
lastPart =
|
|
1727
|
+
partType === "output_text"
|
|
1728
|
+
? { type: "output_text", text: "", annotations: [] }
|
|
1729
|
+
: { type: "refusal", refusal: "" };
|
|
1730
|
+
item.content.push(lastPart);
|
|
1731
|
+
}
|
|
1732
|
+
block.text += delta;
|
|
1733
|
+
if (lastPart.type === "output_text") {
|
|
1734
|
+
lastPart.text += delta;
|
|
1735
|
+
} else {
|
|
1736
|
+
lastPart.refusal += delta;
|
|
1737
|
+
}
|
|
1738
|
+
stream.push({ type: "text_delta", contentIndex, delta, partial: output });
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
export function accumulateToolCallArgumentsDelta(
|
|
1742
|
+
block: ResponsesToolCallBlock,
|
|
1743
|
+
delta: string,
|
|
1744
|
+
stream: AssistantMessageEventStream,
|
|
1745
|
+
output: AssistantMessage,
|
|
1746
|
+
contentIndex: number,
|
|
1747
|
+
): void {
|
|
1748
|
+
block[kStreamingPartialJson] += delta;
|
|
1749
|
+
const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
|
|
1750
|
+
if (throttled) {
|
|
1751
|
+
block.arguments = throttled.value;
|
|
1752
|
+
block[kStreamingLastParseLen] = throttled.parsedLen;
|
|
1753
|
+
}
|
|
1754
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Finalize streamed function-call arguments from the authoritative `.done`
|
|
1759
|
+
* payload. The caller owns the `argumentsDone` flag (generic Responses sets it;
|
|
1760
|
+
* Codex's block shape has no such field), so this only rewrites `arguments` and
|
|
1761
|
+
* drops the transient accumulation fields.
|
|
1762
|
+
*/
|
|
1763
|
+
export function finalizeToolCallArgumentsDone(block: ResponsesToolCallBlock, args: string): void {
|
|
1764
|
+
block[kStreamingPartialJson] = args;
|
|
1765
|
+
block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
|
|
1766
|
+
clearStreamingPartialJson(block);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
export function accumulateCustomToolCallInputDelta(
|
|
1770
|
+
block: ResponsesToolCallBlock,
|
|
1771
|
+
delta: string,
|
|
1772
|
+
stream: AssistantMessageEventStream,
|
|
1773
|
+
output: AssistantMessage,
|
|
1774
|
+
contentIndex: number,
|
|
1775
|
+
): void {
|
|
1776
|
+
block[kStreamingPartialJson] += delta;
|
|
1777
|
+
block.arguments = { input: block[kStreamingPartialJson] };
|
|
1778
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta, partial: output });
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
export function finalizeCustomToolCallInputDone(block: ResponsesToolCallBlock, input: string): void {
|
|
1782
|
+
block[kStreamingPartialJson] = input;
|
|
1783
|
+
block.arguments = { input };
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
export interface ProcessResponsesStreamOptions {
|
|
1787
|
+
onFirstToken?: () => void;
|
|
1788
|
+
onOutputItemDone?: (item: ResponseOutputItem) => void;
|
|
1789
|
+
/**
|
|
1790
|
+
* Called when a terminal `response.completed` or `response.incomplete` event
|
|
1791
|
+
* is successfully processed. Only invoked on the successful-completion path;
|
|
1792
|
+
* thrown failure (`response.failed`) and cancellation paths never call this.
|
|
1793
|
+
* Used by callers to detect premature stream closure (i.e. the stream ended
|
|
1794
|
+
* without a recognized terminal event).
|
|
1795
|
+
*/
|
|
1796
|
+
onCompleted?: () => void;
|
|
1797
|
+
/**
|
|
1798
|
+
* Caller-requested service tier, used to bill the served tier when the
|
|
1799
|
+
* response omits the `service_tier` echo. Only applied for `provider: "openai"`.
|
|
1800
|
+
*/
|
|
1801
|
+
requestServiceTier?: ServiceTier;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
export async function processResponsesStream<TApi extends Api>(
|
|
1805
|
+
openaiStream: AsyncIterable<ResponseStreamEvent>,
|
|
1806
|
+
output: AssistantMessage,
|
|
1807
|
+
stream: AssistantMessageEventStream,
|
|
1808
|
+
model: Model<TApi>,
|
|
1809
|
+
options?: ProcessResponsesStreamOptions,
|
|
1810
|
+
): Promise<void> {
|
|
1811
|
+
type StreamingToolCallBlock = ToolCall & {
|
|
1812
|
+
[kStreamingPartialJson]: string;
|
|
1813
|
+
[kStreamingLastParseLen]?: number;
|
|
1814
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
1815
|
+
};
|
|
1816
|
+
interface StreamingItem {
|
|
1817
|
+
item: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
1818
|
+
block: ThinkingContent | TextContent | StreamingToolCallBlock;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Multiple items (parallel function_calls in particular) can be open at the same
|
|
1822
|
+
// time. OpenAI's spec routes every per-item event by `output_index`/`item_id`;
|
|
1823
|
+
// see https://github.com/can1357/oh-my-pi/issues/1880 — llama.cpp emits parallel
|
|
1824
|
+
// function_call deltas interleaved, and a singleton `current` reference would
|
|
1825
|
+
// fold them into the wrong block and drop arguments on every call but the last.
|
|
1826
|
+
//
|
|
1827
|
+
// OpenAI-compatible hosts can compound this by omitting `item.id` and
|
|
1828
|
+
// `output_index` on `output_item.added` while routing later argument deltas to
|
|
1829
|
+
// either the bare `call_id` or a synthesized `fc_<call_id>` item id. Register
|
|
1830
|
+
// both keys so each delta reaches its own block instead of falling back to the
|
|
1831
|
+
// most recently added parallel call.
|
|
1832
|
+
const openItemsByOutputIndex = new Map<number, StreamingItem>();
|
|
1833
|
+
const openItemsByItemId = new Map<string, StreamingItem>();
|
|
1834
|
+
const openItemsByPrefixedCallId = new Map<string, StreamingItem>();
|
|
1835
|
+
let lastOpenItem: StreamingItem | null = null;
|
|
1836
|
+
const openItemsInOrder: StreamingItem[] = [];
|
|
1837
|
+
|
|
1838
|
+
const prefixedFunctionCallItemKey = (callId: string | undefined): string | undefined =>
|
|
1839
|
+
callId ? `fc_${callId}` : undefined;
|
|
1840
|
+
|
|
1841
|
+
const registerOpenItem = (
|
|
1842
|
+
outputIndex: number | undefined,
|
|
1843
|
+
itemId: string | undefined,
|
|
1844
|
+
entry: StreamingItem,
|
|
1845
|
+
alternateItemKey?: string,
|
|
1846
|
+
prefixedAlternateItemKey?: string,
|
|
1847
|
+
): void => {
|
|
1848
|
+
if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
|
|
1849
|
+
if (itemId) openItemsByItemId.set(itemId, entry);
|
|
1850
|
+
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.set(alternateItemKey, entry);
|
|
1851
|
+
if (
|
|
1852
|
+
prefixedAlternateItemKey &&
|
|
1853
|
+
prefixedAlternateItemKey !== itemId &&
|
|
1854
|
+
prefixedAlternateItemKey !== alternateItemKey
|
|
1855
|
+
) {
|
|
1856
|
+
openItemsByPrefixedCallId.set(prefixedAlternateItemKey, entry);
|
|
1857
|
+
}
|
|
1858
|
+
openItemsInOrder.push(entry);
|
|
1859
|
+
lastOpenItem = entry;
|
|
1860
|
+
};
|
|
1861
|
+
const lookupOpenItem = (event: { output_index?: number; item_id?: string }): StreamingItem | undefined => {
|
|
1862
|
+
const hasKey = typeof event.output_index === "number" || event.item_id !== undefined;
|
|
1863
|
+
if (typeof event.output_index === "number") {
|
|
1864
|
+
const found = openItemsByOutputIndex.get(event.output_index);
|
|
1865
|
+
if (found) return found;
|
|
1866
|
+
}
|
|
1867
|
+
if (event.item_id) {
|
|
1868
|
+
const found = openItemsByItemId.get(event.item_id);
|
|
1869
|
+
if (found) return found;
|
|
1870
|
+
}
|
|
1871
|
+
// Keyed events whose item already closed are stale; drop them instead of
|
|
1872
|
+
// routing to a sibling. Only fully identifierless mock/proxy events use the
|
|
1873
|
+
// legacy singleton fallback.
|
|
1874
|
+
return hasKey ? undefined : (lastOpenItem ?? undefined);
|
|
1875
|
+
};
|
|
1876
|
+
const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
|
|
1877
|
+
typeof event.output_index === "number" || event.item_id !== undefined;
|
|
1878
|
+
const lookupOpenToolCallAlias = (
|
|
1879
|
+
event: { output_index?: number; item_id?: string },
|
|
1880
|
+
type: "function_call" | "custom_tool_call",
|
|
1881
|
+
): StreamingItem | undefined => {
|
|
1882
|
+
if (typeof event.output_index === "number") {
|
|
1883
|
+
const byOutputIndex = openItemsByOutputIndex.get(event.output_index);
|
|
1884
|
+
if (byOutputIndex) return byOutputIndex;
|
|
1885
|
+
// A lossy host (llama.cpp/Ollama, issue #2015) can omit `output_index` on
|
|
1886
|
+
// `output_item.added` while still stamping the spec-required field on the
|
|
1887
|
+
// delta. The index was never registered, so fall through to the prefixed
|
|
1888
|
+
// alias / exact item-id maps instead of dropping to `lastOpenItem`.
|
|
1889
|
+
}
|
|
1890
|
+
if (event.item_id) {
|
|
1891
|
+
// Prefixed call-id aliases share the same wire namespace as real call ids.
|
|
1892
|
+
// Argument/input events can use the prefixed form, while final
|
|
1893
|
+
// output_item.done events below use exact call ids; keep aliases in a
|
|
1894
|
+
// separate map so a real `call_id: "fc_x"` cannot overwrite the alias
|
|
1895
|
+
// for `call_id: "x"`.
|
|
1896
|
+
const alias = openItemsByPrefixedCallId.get(event.item_id);
|
|
1897
|
+
if (alias?.item.type === type) return alias;
|
|
1898
|
+
const exact = openItemsByItemId.get(event.item_id);
|
|
1899
|
+
if (exact) return exact;
|
|
1900
|
+
}
|
|
1901
|
+
return lookupOpenItem(event);
|
|
1902
|
+
};
|
|
1903
|
+
const lookupOpenFunctionCallItem = (event: {
|
|
1904
|
+
output_index?: number;
|
|
1905
|
+
item_id?: string;
|
|
1906
|
+
}): StreamingItem | undefined => {
|
|
1907
|
+
if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
|
|
1908
|
+
for (const candidate of openItemsInOrder) {
|
|
1909
|
+
if (
|
|
1910
|
+
candidate.item.type === "function_call" &&
|
|
1911
|
+
candidate.block.type === "toolCall" &&
|
|
1912
|
+
!candidate.block[kStreamingArgumentsDone]
|
|
1913
|
+
) {
|
|
1914
|
+
return candidate;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
|
|
1918
|
+
};
|
|
1919
|
+
const closeOpenItem = (
|
|
1920
|
+
outputIndex: number | undefined,
|
|
1921
|
+
itemId: string | undefined,
|
|
1922
|
+
entry: StreamingItem | undefined,
|
|
1923
|
+
alternateItemKey?: string,
|
|
1924
|
+
prefixedAlternateItemKey?: string,
|
|
1925
|
+
): void => {
|
|
1926
|
+
if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
|
|
1927
|
+
if (itemId) openItemsByItemId.delete(itemId);
|
|
1928
|
+
if (alternateItemKey && alternateItemKey !== itemId) openItemsByItemId.delete(alternateItemKey);
|
|
1929
|
+
if (
|
|
1930
|
+
prefixedAlternateItemKey &&
|
|
1931
|
+
prefixedAlternateItemKey !== itemId &&
|
|
1932
|
+
prefixedAlternateItemKey !== alternateItemKey &&
|
|
1933
|
+
openItemsByPrefixedCallId.get(prefixedAlternateItemKey) === entry
|
|
1934
|
+
) {
|
|
1935
|
+
openItemsByPrefixedCallId.delete(prefixedAlternateItemKey);
|
|
1936
|
+
}
|
|
1937
|
+
if (entry) {
|
|
1938
|
+
const index = openItemsInOrder.indexOf(entry);
|
|
1939
|
+
if (index >= 0) openItemsInOrder.splice(index, 1);
|
|
1940
|
+
}
|
|
1941
|
+
if (entry && lastOpenItem === entry) lastOpenItem = null;
|
|
1942
|
+
};
|
|
1943
|
+
const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
|
|
1944
|
+
output.content.indexOf(block);
|
|
1945
|
+
|
|
1946
|
+
let sawFirstToken = false;
|
|
1947
|
+
|
|
1948
|
+
for await (const event of openaiStream) {
|
|
1949
|
+
if (event.type === "response.created") {
|
|
1950
|
+
output.responseId = event.response.id;
|
|
1951
|
+
} else if (event.type === "response.output_item.added") {
|
|
1952
|
+
if (!sawFirstToken) {
|
|
1953
|
+
sawFirstToken = true;
|
|
1954
|
+
options?.onFirstToken?.();
|
|
1955
|
+
}
|
|
1956
|
+
const item = event.item;
|
|
1957
|
+
if (item.type === "reasoning") {
|
|
1958
|
+
const block: ThinkingContent = { type: "thinking", thinking: "", itemId: item.id };
|
|
1959
|
+
output.content.push(block);
|
|
1960
|
+
registerOpenItem(event.output_index, item.id, { item, block });
|
|
1961
|
+
stream.push({ type: "thinking_start", contentIndex: contentIndexOf(block), partial: output });
|
|
1962
|
+
} else if (item.type === "message") {
|
|
1963
|
+
const block: TextContent = {
|
|
1964
|
+
type: "text",
|
|
1965
|
+
text: "",
|
|
1966
|
+
textSignature: encodeTextSignatureV1(item.id, item.phase ?? undefined),
|
|
1967
|
+
};
|
|
1968
|
+
output.content.push(block);
|
|
1969
|
+
registerOpenItem(event.output_index, item.id, { item, block });
|
|
1970
|
+
stream.push({ type: "text_start", contentIndex: contentIndexOf(block), partial: output });
|
|
1971
|
+
} else if (item.type === "function_call") {
|
|
1972
|
+
const block: StreamingToolCallBlock = {
|
|
1973
|
+
type: "toolCall",
|
|
1974
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1975
|
+
name: item.name,
|
|
1976
|
+
arguments: {},
|
|
1977
|
+
[kStreamingPartialJson]: item.arguments || "",
|
|
1978
|
+
};
|
|
1979
|
+
output.content.push(block);
|
|
1980
|
+
registerOpenItem(
|
|
1981
|
+
event.output_index,
|
|
1982
|
+
item.id,
|
|
1983
|
+
{ item, block },
|
|
1984
|
+
item.call_id,
|
|
1985
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
1986
|
+
);
|
|
1987
|
+
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
1988
|
+
} else if (item.type === "custom_tool_call") {
|
|
1989
|
+
const block: StreamingToolCallBlock = {
|
|
1990
|
+
type: "toolCall",
|
|
1991
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1992
|
+
// Preserve the raw wire name (e.g. `apply_patch`). The agent-loop
|
|
1993
|
+
// dispatcher matches it against both `Tool.name` and
|
|
1994
|
+
// `Tool.customWireName`, so this stays wire-accurate through
|
|
1995
|
+
// history replay while still routing to the right handler.
|
|
1996
|
+
name: item.name,
|
|
1997
|
+
arguments: { input: item.input ?? "" },
|
|
1998
|
+
customWireName: item.name,
|
|
1999
|
+
// Custom tools stream a raw string, but we reuse `partialJson` as the
|
|
2000
|
+
// accumulation buffer so later code that inspects the field still works.
|
|
2001
|
+
[kStreamingPartialJson]: item.input ?? "",
|
|
2002
|
+
};
|
|
2003
|
+
output.content.push(block);
|
|
2004
|
+
registerOpenItem(
|
|
2005
|
+
event.output_index,
|
|
2006
|
+
item.id,
|
|
2007
|
+
{ item, block },
|
|
2008
|
+
item.call_id,
|
|
2009
|
+
prefixedFunctionCallItemKey(item.call_id),
|
|
2010
|
+
);
|
|
2011
|
+
stream.push({ type: "toolcall_start", contentIndex: contentIndexOf(block), partial: output });
|
|
2012
|
+
}
|
|
2013
|
+
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
2014
|
+
const entry = lookupOpenItem(event);
|
|
2015
|
+
if (entry?.item.type === "reasoning") appendReasoningSummaryPart(entry.item, event.part);
|
|
2016
|
+
} else if (event.type === "response.reasoning_summary_text.delta") {
|
|
2017
|
+
const entry = lookupOpenItem(event);
|
|
2018
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2019
|
+
appendReasoningSummaryTextDelta(
|
|
2020
|
+
entry.item,
|
|
2021
|
+
entry.block,
|
|
2022
|
+
event.delta,
|
|
2023
|
+
stream,
|
|
2024
|
+
output,
|
|
2025
|
+
contentIndexOf(entry.block),
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
} else if (event.type === "response.reasoning_summary_part.done") {
|
|
2029
|
+
const entry = lookupOpenItem(event);
|
|
2030
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2031
|
+
appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, contentIndexOf(entry.block));
|
|
2032
|
+
}
|
|
2033
|
+
} else if (event.type === "response.reasoning_text.delta") {
|
|
2034
|
+
// Raw reasoning text delta from local providers that stream thinking
|
|
2035
|
+
// directly rather than via the OpenAI summary tracking protocol.
|
|
2036
|
+
const entry = lookupOpenItem(event);
|
|
2037
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
2038
|
+
entry.block.thinking += event.delta;
|
|
2039
|
+
stream.push({
|
|
2040
|
+
type: "thinking_delta",
|
|
2041
|
+
contentIndex: contentIndexOf(entry.block),
|
|
2042
|
+
delta: event.delta,
|
|
2043
|
+
partial: output,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
} else if (event.type === "response.content_part.added") {
|
|
2047
|
+
const entry = lookupOpenItem(event);
|
|
2048
|
+
if (entry?.item.type === "message") appendMessageContentPart(entry.item, event.part);
|
|
2049
|
+
} else if (event.type === "response.output_text.delta") {
|
|
2050
|
+
const entry = lookupOpenItem(event);
|
|
2051
|
+
if (entry?.item.type === "message" && entry.block.type === "text") {
|
|
2052
|
+
appendMessageTextDelta(
|
|
2053
|
+
entry.item,
|
|
2054
|
+
entry.block,
|
|
2055
|
+
event.delta,
|
|
2056
|
+
stream,
|
|
2057
|
+
output,
|
|
2058
|
+
contentIndexOf(entry.block),
|
|
2059
|
+
"output_text",
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
} else if (event.type === "response.refusal.delta") {
|
|
2063
|
+
const entry = lookupOpenItem(event);
|
|
2064
|
+
if (entry?.item.type === "message" && entry.block.type === "text") {
|
|
2065
|
+
appendMessageTextDelta(
|
|
2066
|
+
entry.item,
|
|
2067
|
+
entry.block,
|
|
2068
|
+
event.delta,
|
|
2069
|
+
stream,
|
|
2070
|
+
output,
|
|
2071
|
+
contentIndexOf(entry.block),
|
|
2072
|
+
"refusal",
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
} else if (event.type === "response.function_call_arguments.delta") {
|
|
2076
|
+
const entry = lookupOpenFunctionCallItem(event);
|
|
2077
|
+
if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
|
|
2078
|
+
accumulateToolCallArgumentsDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
|
|
2079
|
+
}
|
|
2080
|
+
} else if (event.type === "response.function_call_arguments.done") {
|
|
2081
|
+
const entry = lookupOpenFunctionCallItem(event);
|
|
2082
|
+
if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
|
|
2083
|
+
finalizeToolCallArgumentsDone(entry.block, event.arguments);
|
|
2084
|
+
entry.block[kStreamingArgumentsDone] = true;
|
|
2085
|
+
}
|
|
2086
|
+
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
2087
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
2088
|
+
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
2089
|
+
accumulateCustomToolCallInputDelta(entry.block, event.delta, stream, output, contentIndexOf(entry.block));
|
|
2090
|
+
}
|
|
2091
|
+
} else if (event.type === "response.custom_tool_call_input.done") {
|
|
2092
|
+
const entry = lookupOpenToolCallAlias(event, "custom_tool_call");
|
|
2093
|
+
if (entry?.item.type === "custom_tool_call" && entry.block.type === "toolCall") {
|
|
2094
|
+
finalizeCustomToolCallInputDone(entry.block, event.input);
|
|
2095
|
+
}
|
|
2096
|
+
} else if (event.type === "response.output_item.done") {
|
|
2097
|
+
const item = structuredCloneJSON(event.item);
|
|
2098
|
+
options?.onOutputItemDone?.(item);
|
|
2099
|
+
const entry =
|
|
2100
|
+
item.type === "function_call" || item.type === "custom_tool_call"
|
|
2101
|
+
? lookupOpenItem({ output_index: event.output_index, item_id: item.id ?? item.call_id })
|
|
2102
|
+
: lookupOpenItem({ output_index: event.output_index, item_id: item.id });
|
|
2103
|
+
if (item.type === "reasoning") {
|
|
2104
|
+
const thinking =
|
|
2105
|
+
item.summary?.length > 0
|
|
2106
|
+
? item.summary.map(part => part.text).join("\n\n")
|
|
2107
|
+
: item.content?.[0]?.type === "reasoning_text"
|
|
2108
|
+
? (item.content[0].text ?? "")
|
|
2109
|
+
: "";
|
|
2110
|
+
// Prefer the routed entry; the bare itemId find misroutes when ids are
|
|
2111
|
+
// absent (`undefined === undefined` matches the FIRST thinking block) and
|
|
2112
|
+
// misses entirely when the done-event id drifts from the added-event id.
|
|
2113
|
+
const reasoningBlock =
|
|
2114
|
+
entry?.block.type === "thinking"
|
|
2115
|
+
? entry.block
|
|
2116
|
+
: (output.content.find(b => b.type === "thinking" && (b as ThinkingContent).itemId === item.id) as
|
|
2117
|
+
| ThinkingContent
|
|
2118
|
+
| undefined);
|
|
2119
|
+
if (reasoningBlock) {
|
|
2120
|
+
reasoningBlock.thinking = thinking;
|
|
2121
|
+
reasoningBlock.thinkingSignature = JSON.stringify(item);
|
|
2122
|
+
stream.push({
|
|
2123
|
+
type: "thinking_end",
|
|
2124
|
+
contentIndex: contentIndexOf(reasoningBlock),
|
|
2125
|
+
content: thinking,
|
|
2126
|
+
partial: output,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
closeOpenItem(event.output_index, item.id, entry);
|
|
2130
|
+
} else if (item.type === "message") {
|
|
2131
|
+
const block = entry?.block.type === "text" ? entry.block : undefined;
|
|
2132
|
+
const text = item.content
|
|
2133
|
+
.map(part => (part.type === "output_text" ? (part.text ?? "") : (part.refusal ?? "")))
|
|
2134
|
+
.join("");
|
|
2135
|
+
const textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
|
|
2136
|
+
let contentIndex: number;
|
|
2137
|
+
if (block) {
|
|
2138
|
+
block.text = text;
|
|
2139
|
+
block.textSignature = textSignature;
|
|
2140
|
+
contentIndex = contentIndexOf(block);
|
|
2141
|
+
} else {
|
|
2142
|
+
// `output_item.added` never arrived (lossy proxy) — synthesize the
|
|
2143
|
+
// block so the final message still carries the authoritative text.
|
|
2144
|
+
const synthesized: TextContent = { type: "text", text, textSignature };
|
|
2145
|
+
output.content.push(synthesized);
|
|
2146
|
+
contentIndex = output.content.length - 1;
|
|
2147
|
+
}
|
|
2148
|
+
stream.push({ type: "text_end", contentIndex, content: text, partial: output });
|
|
2149
|
+
closeOpenItem(event.output_index, item.id, entry);
|
|
2150
|
+
} else if (item.type === "function_call") {
|
|
2151
|
+
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2152
|
+
const args = block?.[kStreamingArgumentsDone]
|
|
2153
|
+
? block.arguments
|
|
2154
|
+
: block?.[kStreamingPartialJson]
|
|
2155
|
+
? parseStreamingJson(block[kStreamingPartialJson])
|
|
2156
|
+
: parseStreamingJson(item.arguments || "{}");
|
|
2157
|
+
const toolCall: ToolCall = {
|
|
2158
|
+
type: "toolCall",
|
|
2159
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2160
|
+
name: item.name,
|
|
2161
|
+
arguments: args,
|
|
2162
|
+
};
|
|
2163
|
+
let contentIndex: number;
|
|
2164
|
+
if (block) {
|
|
2165
|
+
// Persist the authoritative final args on the stored block. The
|
|
2166
|
+
// throttled delta parser may have skipped the last partial parse,
|
|
2167
|
+
// leaving block.arguments stale (often `{}`); the emitted toolCall
|
|
2168
|
+
// and the persisted block must agree.
|
|
2169
|
+
block.arguments = args;
|
|
2170
|
+
clearStreamingPartialJson(block);
|
|
2171
|
+
contentIndex = contentIndexOf(block);
|
|
2172
|
+
} else {
|
|
2173
|
+
// `output_item.added` never arrived (lossy proxy) — synthesize the
|
|
2174
|
+
// block so the final message carries the call the consumer was told
|
|
2175
|
+
// completed (the agent loop executes tools from message.content).
|
|
2176
|
+
output.content.push(toolCall);
|
|
2177
|
+
contentIndex = output.content.length - 1;
|
|
2178
|
+
}
|
|
2179
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2180
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2181
|
+
} else if (item.type === "custom_tool_call") {
|
|
2182
|
+
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2183
|
+
const rawInput = block?.[kStreamingPartialJson] ? block[kStreamingPartialJson] : (item.input ?? "");
|
|
2184
|
+
const toolCall: ToolCall = {
|
|
2185
|
+
type: "toolCall",
|
|
2186
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2187
|
+
name: item.name,
|
|
2188
|
+
arguments: { input: rawInput },
|
|
2189
|
+
customWireName: item.name,
|
|
2190
|
+
};
|
|
2191
|
+
let contentIndex: number;
|
|
2192
|
+
if (block) {
|
|
2193
|
+
// Persist the final input on the stored block and drop the transient
|
|
2194
|
+
// accumulation buffer, mirroring the function_call branch above.
|
|
2195
|
+
block.arguments = { input: rawInput };
|
|
2196
|
+
clearStreamingPartialJson(block);
|
|
2197
|
+
contentIndex = contentIndexOf(block);
|
|
2198
|
+
} else {
|
|
2199
|
+
output.content.push(toolCall);
|
|
2200
|
+
contentIndex = output.content.length - 1;
|
|
2201
|
+
}
|
|
2202
|
+
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2203
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2204
|
+
}
|
|
2205
|
+
} else if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
2206
|
+
const response = event.response;
|
|
2207
|
+
finalizePendingResponsesToolCalls(output);
|
|
2208
|
+
if (response?.id) {
|
|
2209
|
+
output.responseId = response.id;
|
|
2210
|
+
}
|
|
2211
|
+
populateResponsesUsageFromResponse(output, response?.usage);
|
|
2212
|
+
calculateCost(model, output.usage);
|
|
2213
|
+
applyOpenAIResponsesServiceTierCost(
|
|
2214
|
+
model,
|
|
2215
|
+
output.usage,
|
|
2216
|
+
(response as { service_tier?: unknown } | undefined)?.service_tier,
|
|
2217
|
+
options?.requestServiceTier,
|
|
2218
|
+
);
|
|
2219
|
+
output.stopReason = mapOpenAIResponsesStopReason(response?.status);
|
|
2220
|
+
if (response?.status === "failed" || response?.status === "cancelled") {
|
|
2221
|
+
const error = response?.error ?? (response as any)?.status_details?.error;
|
|
2222
|
+
const details = response?.incomplete_details;
|
|
2223
|
+
const statusDetailsReason = (response as any)?.status_details?.reason;
|
|
2224
|
+
const message = error
|
|
2225
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
2226
|
+
: details?.reason
|
|
2227
|
+
? `incomplete: ${details.reason}`
|
|
2228
|
+
: typeof statusDetailsReason === "string" && statusDetailsReason.length > 0
|
|
2229
|
+
? `status_details: ${statusDetailsReason}`
|
|
2230
|
+
: "Unknown error (no error details in response)";
|
|
2231
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
2232
|
+
}
|
|
2233
|
+
if (response?.status === "incomplete" && response.incomplete_details?.reason === "content_filter") {
|
|
2234
|
+
// A content-filtered turn is a failure, not a token-cap truncation —
|
|
2235
|
+
// mapping it to "length" would route the agent loop into "shorten your
|
|
2236
|
+
// output" recovery against a filtered prompt.
|
|
2237
|
+
throw new AIError.ProviderResponseError("incomplete: content_filter", {
|
|
2238
|
+
provider: model.provider,
|
|
2239
|
+
kind: "content-blocked",
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
promoteResponsesToolUseStopReason(output, (response as { end_turn?: boolean } | undefined)?.end_turn);
|
|
2243
|
+
options?.onCompleted?.();
|
|
2244
|
+
// `response.completed`/`response.incomplete` is the last event of a
|
|
2245
|
+
// Responses stream. Stop pulling instead of waiting for the server to
|
|
2246
|
+
// close the connection: misbehaving providers keep the socket open
|
|
2247
|
+
// after the terminal event, which would park this loop until the idle
|
|
2248
|
+
// watchdog converts an already-successful turn into a timeout error.
|
|
2249
|
+
// Breaking unwinds the iterator chain (the consumer's `.return()`
|
|
2250
|
+
// reaches the SDK stream), actively releasing the connection.
|
|
2251
|
+
break;
|
|
2252
|
+
} else if (event.type === "error") {
|
|
2253
|
+
const err = (event as any).error ?? event;
|
|
2254
|
+
const code = err.code ?? "unknown";
|
|
2255
|
+
const message = err.message ?? "no message";
|
|
2256
|
+
throw new AIError.ProviderResponseError(`Error Code ${code}: ${message}`, {
|
|
2257
|
+
provider: model.provider,
|
|
2258
|
+
kind: "output",
|
|
2259
|
+
});
|
|
2260
|
+
} else if (event.type === "response.failed") {
|
|
2261
|
+
populateResponsesUsageFromResponse(output, event.response?.usage);
|
|
2262
|
+
const error = event.response?.error ?? (event.response as any)?.status_details?.error;
|
|
2263
|
+
const details = event.response?.incomplete_details;
|
|
2264
|
+
const message = error
|
|
2265
|
+
? `${error.code || "unknown"}: ${error.message || "no message"}`
|
|
2266
|
+
: details?.reason
|
|
2267
|
+
? `incomplete: ${details.reason}`
|
|
2268
|
+
: "Unknown error (no error details in response)";
|
|
2269
|
+
throw new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
export function mapOpenAIResponsesStopReason(status: ResponseStatus | undefined): StopReason {
|
|
2275
|
+
if (!status) return "stop";
|
|
2276
|
+
switch (status) {
|
|
2277
|
+
case "completed":
|
|
2278
|
+
return "stop";
|
|
2279
|
+
case "incomplete":
|
|
2280
|
+
return "length";
|
|
2281
|
+
case "failed":
|
|
2282
|
+
case "cancelled":
|
|
2283
|
+
return "error";
|
|
2284
|
+
case "in_progress":
|
|
2285
|
+
case "queued":
|
|
2286
|
+
return "stop";
|
|
2287
|
+
default: {
|
|
2288
|
+
// Compile-time exhaustiveness; at runtime a brand-new status from the
|
|
2289
|
+
// server must degrade gracefully instead of failing a fully-streamed
|
|
2290
|
+
// response.
|
|
2291
|
+
const exhaustive: never = status;
|
|
2292
|
+
logger.warn("Unhandled OpenAI Responses stop reason", { status: exhaustive });
|
|
2293
|
+
return "stop";
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* Finalize any streamed toolCall block whose `output_item.done` never arrived
|
|
2300
|
+
* (lossy proxy, or a terminal event that raced the per-item done): parse the
|
|
2301
|
+
* accumulated `partialJson` into authoritative arguments and strip the transient
|
|
2302
|
+
* streaming fields so they never persist. Shared by the chat-Responses decoder
|
|
2303
|
+
* and the Codex decoder. Closed blocks already cleared these fields, so walking
|
|
2304
|
+
* the full content list leaves them untouched.
|
|
2305
|
+
*/
|
|
2306
|
+
export function finalizePendingResponsesToolCalls(output: AssistantMessage): void {
|
|
2307
|
+
for (const block of output.content) {
|
|
2308
|
+
if (block.type !== "toolCall") continue;
|
|
2309
|
+
const pending = block as ToolCall & {
|
|
2310
|
+
[kStreamingPartialJson]?: string;
|
|
2311
|
+
[kStreamingLastParseLen]?: number;
|
|
2312
|
+
[kStreamingArgumentsDone]?: boolean;
|
|
2313
|
+
};
|
|
2314
|
+
if (pending[kStreamingPartialJson] && !pending[kStreamingArgumentsDone]) {
|
|
2315
|
+
pending.arguments =
|
|
2316
|
+
pending.customWireName !== undefined
|
|
2317
|
+
? { input: pending[kStreamingPartialJson] }
|
|
2318
|
+
: parseStreamingJson(pending[kStreamingPartialJson]);
|
|
2319
|
+
}
|
|
2320
|
+
clearStreamingPartialJson(pending);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
/**
|
|
2325
|
+
* Apply the Responses terminal stop-reason invariants shared by the chat-Responses
|
|
2326
|
+
* and Codex decoders: a turn that produced tool calls becomes `toolUse`, and a
|
|
2327
|
+
* Codex-lineage `end_turn: false` marker pauses the turn so the agent loop
|
|
2328
|
+
* re-samples instead of ending. Callers set `output.stopReason` from the wire
|
|
2329
|
+
* status first via {@link mapOpenAIResponsesStopReason}.
|
|
2330
|
+
*/
|
|
2331
|
+
export function promoteResponsesToolUseStopReason(output: AssistantMessage, endTurn: boolean | undefined): void {
|
|
2332
|
+
if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
|
|
2333
|
+
output.stopReason = "toolUse";
|
|
2334
|
+
}
|
|
2335
|
+
if (endTurn === false && output.stopReason === "stop") {
|
|
2336
|
+
output.stopDetails = { type: "pause_turn" };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
/** Initial empty `AssistantMessage` that streaming providers accumulate into. */
|
|
2341
|
+
export function createInitialResponsesAssistantMessage(api: Api, provider: string, modelId: string): AssistantMessage {
|
|
2342
|
+
return {
|
|
2343
|
+
role: "assistant",
|
|
2344
|
+
content: [],
|
|
2345
|
+
api,
|
|
2346
|
+
provider,
|
|
2347
|
+
model: modelId,
|
|
2348
|
+
usage: {
|
|
2349
|
+
input: 0,
|
|
2350
|
+
output: 0,
|
|
2351
|
+
cacheRead: 0,
|
|
2352
|
+
cacheWrite: 0,
|
|
2353
|
+
totalTokens: 0,
|
|
2354
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
2355
|
+
},
|
|
2356
|
+
stopReason: "stop",
|
|
2357
|
+
timestamp: Date.now(),
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/** Extension fields we add on top of `ResponseCreateParamsStreaming` across the Responses-family providers. */
|
|
2362
|
+
export type ResponsesSamplingParamsExtras = {
|
|
2363
|
+
top_p?: number;
|
|
2364
|
+
top_k?: number;
|
|
2365
|
+
min_p?: number;
|
|
2366
|
+
presence_penalty?: number;
|
|
2367
|
+
repetition_penalty?: number;
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
type CommonResponsesParams = ResponseCreateParamsStreaming & ResponsesSamplingParamsExtras;
|
|
2371
|
+
|
|
2372
|
+
type CommonSamplingOptions = Pick<
|
|
2373
|
+
StreamOptions,
|
|
2374
|
+
"temperature" | "topP" | "topK" | "minP" | "presencePenalty" | "repetitionPenalty" | "maxTokens"
|
|
2375
|
+
> & { serviceTier?: ServiceTier };
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
|
|
2379
|
+
* temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
|
|
2380
|
+
*
|
|
2381
|
+
* `max_output_tokens` is suppressed when {@link Model.omitMaxOutputTokens} is `true`, so
|
|
2382
|
+
* proxies (notably Ollama) that forward to upstream APIs with an unknown output-token cap
|
|
2383
|
+
* can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
|
|
2384
|
+
* reflect the model's context window rather than the upstream output limit.
|
|
2385
|
+
*/
|
|
2386
|
+
export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
|
|
2387
|
+
params: P,
|
|
2388
|
+
options: CommonSamplingOptions | undefined,
|
|
2389
|
+
model: Pick<Model, "provider" | "omitMaxOutputTokens" | "maxTokens">,
|
|
2390
|
+
): void {
|
|
2391
|
+
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
|
2392
|
+
params.max_output_tokens = Math.min(
|
|
2393
|
+
options.maxTokens,
|
|
2394
|
+
model.maxTokens ?? Number.POSITIVE_INFINITY,
|
|
2395
|
+
OPENAI_MAX_OUTPUT_TOKENS,
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
2399
|
+
if (options?.topP !== undefined) params.top_p = options.topP;
|
|
2400
|
+
if (options?.topK !== undefined) params.top_k = options.topK;
|
|
2401
|
+
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
2402
|
+
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
2403
|
+
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
2404
|
+
applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
const RESPONSES_REASONING_SUPPRESSION_PROMPT = responsesReasoningSuppressionPrompt.trim();
|
|
2408
|
+
|
|
2409
|
+
type ReasoningOptions = {
|
|
2410
|
+
reasoning?: string;
|
|
2411
|
+
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
2412
|
+
disableReasoning?: boolean;
|
|
2413
|
+
toolChoice?: unknown;
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
export interface ApplyResponsesCompatPolicyOptions {
|
|
2417
|
+
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
2418
|
+
mapEffort?: (effort: string) => string;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(
|
|
2422
|
+
params: P,
|
|
2423
|
+
messages: ResponseInput,
|
|
2424
|
+
policy: OpenAICompatPolicy,
|
|
2425
|
+
options: ApplyResponsesCompatPolicyOptions | undefined,
|
|
2426
|
+
): number {
|
|
2427
|
+
const reasoning = policy.reasoning;
|
|
2428
|
+
if (!reasoning.modelSupported) return 0;
|
|
2429
|
+
if (reasoning.includeEncryptedReasoning) {
|
|
2430
|
+
const include = params.include ?? [];
|
|
2431
|
+
if (!include.includes("reasoning.encrypted_content")) include.push("reasoning.encrypted_content");
|
|
2432
|
+
params.include = include;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (reasoning.disabled) {
|
|
2436
|
+
if (reasoning.disableMode === "openrouter-enabled-false") {
|
|
2437
|
+
params.reasoning = { enabled: false } as P["reasoning"];
|
|
2438
|
+
return 0;
|
|
2439
|
+
}
|
|
2440
|
+
if (
|
|
2441
|
+
reasoning.disableMode === "lowest-effort" &&
|
|
2442
|
+
reasoning.wireEffort !== undefined &&
|
|
2443
|
+
!reasoning.omitReasoningEffort
|
|
2444
|
+
) {
|
|
2445
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
2446
|
+
params.reasoning = { effort: reasoning.wireEffort as ReasoningParam["effort"] } as P["reasoning"] &
|
|
2447
|
+
ReasoningParam;
|
|
2448
|
+
return 0;
|
|
2449
|
+
}
|
|
2450
|
+
if (policy.compat.requiresReasoningSuppressionPrompt && reasoning.requestedEffort === undefined) {
|
|
2451
|
+
messages.push({
|
|
2452
|
+
role: "developer",
|
|
2453
|
+
content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
|
|
2454
|
+
});
|
|
2455
|
+
return 1;
|
|
2456
|
+
}
|
|
2457
|
+
return 0;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
if (reasoning.requestedEffort !== undefined || options?.reasoningSummary !== undefined) {
|
|
2461
|
+
if (reasoning.omitReasoningEffort) {
|
|
2462
|
+
if (options?.reasoningSummary !== undefined && options.reasoningSummary !== null) {
|
|
2463
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
2464
|
+
params.reasoning = { summary: options.reasoningSummary || "auto" } as P["reasoning"] & ReasoningParam;
|
|
2465
|
+
}
|
|
2466
|
+
return 0;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
const requested = reasoning.requestedEffort ?? "medium";
|
|
2470
|
+
const wireEffort = reasoning.wireEffort ?? options?.mapEffort?.(requested) ?? requested;
|
|
2471
|
+
type ReasoningParam = NonNullable<ResponseCreateParamsStreaming["reasoning"]>;
|
|
2472
|
+
const reasoningParams: ReasoningParam = {
|
|
2473
|
+
effort: wireEffort as ReasoningParam["effort"],
|
|
2474
|
+
};
|
|
2475
|
+
if (options?.reasoningSummary !== null) {
|
|
2476
|
+
reasoningParams.summary = options?.reasoningSummary || "auto";
|
|
2477
|
+
}
|
|
2478
|
+
params.reasoning = reasoningParams as P["reasoning"];
|
|
2479
|
+
return 0;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (policy.compat.requiresReasoningSuppressionPrompt) {
|
|
2483
|
+
messages.push({
|
|
2484
|
+
role: "developer",
|
|
2485
|
+
content: [{ type: "input_text", text: RESPONSES_REASONING_SUPPRESSION_PROMPT }],
|
|
2486
|
+
});
|
|
2487
|
+
return 1;
|
|
2488
|
+
}
|
|
2489
|
+
return 0;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* Apply reasoning-related Responses parameters. Default behavior comes from
|
|
2494
|
+
* catalog compat; include/omit arguments are explicit adapter-wrapper overrides.
|
|
2495
|
+
*/
|
|
2496
|
+
export function applyResponsesReasoningParams<P extends ResponseCreateParamsStreaming>(
|
|
2497
|
+
params: P,
|
|
2498
|
+
model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
|
|
2499
|
+
options: ReasoningOptions | undefined,
|
|
2500
|
+
messages: ResponseInput,
|
|
2501
|
+
mapEffort?: (effort: string) => string,
|
|
2502
|
+
includeEncryptedReasoning?: boolean,
|
|
2503
|
+
omitReasoningEffort?: boolean,
|
|
2504
|
+
): number {
|
|
2505
|
+
return applyResponsesCompatPolicy(
|
|
2506
|
+
params,
|
|
2507
|
+
messages,
|
|
2508
|
+
resolveOpenAICompatPolicy(model, {
|
|
2509
|
+
endpoint: "responses",
|
|
2510
|
+
reasoning: options?.reasoning,
|
|
2511
|
+
disableReasoning: options?.disableReasoning,
|
|
2512
|
+
toolChoice: options?.toolChoice,
|
|
2513
|
+
includeEncryptedReasoning,
|
|
2514
|
+
omitReasoningEffort,
|
|
2515
|
+
}),
|
|
2516
|
+
{ reasoningSummary: options?.reasoningSummary, mapEffort },
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/** Populate `output.usage` from a Responses-API `response.usage` payload. Does not invoke `calculateCost`. */
|
|
2521
|
+
export function populateResponsesUsageFromResponse(
|
|
2522
|
+
output: AssistantMessage,
|
|
2523
|
+
usage:
|
|
2524
|
+
| {
|
|
2525
|
+
input_tokens?: number | null;
|
|
2526
|
+
output_tokens?: number | null;
|
|
2527
|
+
total_tokens?: number | null;
|
|
2528
|
+
prompt_cache_hit_tokens?: number | null;
|
|
2529
|
+
prompt_cache_miss_tokens?: number | null;
|
|
2530
|
+
input_tokens_details?: {
|
|
2531
|
+
cached_tokens?: number | null;
|
|
2532
|
+
cache_write_tokens?: number | null;
|
|
2533
|
+
orchestration_input_tokens?: number | null;
|
|
2534
|
+
orchestration_input_cached_tokens?: number | null;
|
|
2535
|
+
} | null;
|
|
2536
|
+
output_tokens_details?: {
|
|
2537
|
+
reasoning_tokens?: number | null;
|
|
2538
|
+
orchestration_output_tokens?: number | null;
|
|
2539
|
+
} | null;
|
|
2540
|
+
}
|
|
2541
|
+
| null
|
|
2542
|
+
| undefined,
|
|
2543
|
+
): void {
|
|
2544
|
+
if (!usage) return;
|
|
2545
|
+
const details = usage.input_tokens_details;
|
|
2546
|
+
const outputDetails = usage.output_tokens_details;
|
|
2547
|
+
const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
|
|
2548
|
+
const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
|
|
2549
|
+
const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
|
|
2550
|
+
const accounting = calculateOpenAIUsageAccounting({
|
|
2551
|
+
promptTokens: (usage.input_tokens ?? 0) + orchestrationInputTokens,
|
|
2552
|
+
outputTokens: (usage.output_tokens ?? 0) + orchestrationOutputTokens,
|
|
2553
|
+
cachedTokens: (details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0) + orchestrationInputCachedTokens,
|
|
2554
|
+
reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
|
|
2555
|
+
cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
|
|
2556
|
+
cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
|
|
2557
|
+
hasDeepSeekCacheHitAndMiss:
|
|
2558
|
+
usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
// Wholesale replacement must not drop provider-annotated extras (Copilot
|
|
2562
|
+
// premium-request accounting): the failed/cancelled paths throw right after
|
|
2563
|
+
// this call with no later chance to re-apply.
|
|
2564
|
+
const premiumRequests = output.usage.premiumRequests;
|
|
2565
|
+
output.usage = {
|
|
2566
|
+
...accounting,
|
|
2567
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
2568
|
+
};
|
|
2569
|
+
if (premiumRequests !== undefined) {
|
|
2570
|
+
output.usage.premiumRequests = premiumRequests;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* Structural equality for the chain prefix/option check, equivalent to the
|
|
2576
|
+
* default {@link Bun.deepEquals} (own enumerable keys, `absent ≡ own-undefined`)
|
|
2577
|
+
* except for two deliberate exclusions:
|
|
2578
|
+
* - **symbol-keyed properties are ignored** — `for…in` walks enumerable
|
|
2579
|
+
* *string* keys only (never symbols); these are plain wire items whose
|
|
2580
|
+
* prototype contributes no enumerable keys, so iteration is effectively
|
|
2581
|
+
* own-string-keyed. That is how the transient streaming symbols
|
|
2582
|
+
* (`block-symbols.ts`) stamped onto live request items are excluded (the
|
|
2583
|
+
* deep-cloned baseline never carries them). Do NOT add an
|
|
2584
|
+
* `Object.getOwnPropertySymbols` pass, or those symbols resurface and break
|
|
2585
|
+
* chaining.
|
|
2586
|
+
* - keys listed in `omitKeys` are skipped (the option compare omits `input`
|
|
2587
|
+
* and the per-turn `client_metadata`).
|
|
2588
|
+
* A defined value differing across sides IS a difference; a key undefined or
|
|
2589
|
+
* absent on both stays equal. Nested values use full {@link Bun.deepEquals}.
|
|
2590
|
+
*/
|
|
2591
|
+
function deepEqualsWithout(a: unknown, b: unknown, omitKeys?: Record<string, boolean>): boolean {
|
|
2592
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return Bun.deepEquals(a, b);
|
|
2593
|
+
const ao = a as Record<string, unknown>;
|
|
2594
|
+
const bo = b as Record<string, unknown>;
|
|
2595
|
+
for (const key in ao) {
|
|
2596
|
+
if (omitKeys?.[key]) continue;
|
|
2597
|
+
const av = ao[key];
|
|
2598
|
+
const bv = bo[key];
|
|
2599
|
+
if (av !== bv && !Bun.deepEquals(av, bv)) return false;
|
|
2600
|
+
}
|
|
2601
|
+
for (const key in bo) {
|
|
2602
|
+
if (omitKeys?.[key]) continue;
|
|
2603
|
+
if (bo[key] !== undefined && !(key in ao)) return false;
|
|
2604
|
+
}
|
|
2605
|
+
return true;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
const TOP_LEVEL_EXCLUDE_MAP = {
|
|
2609
|
+
input: true,
|
|
2610
|
+
client_metadata: true,
|
|
2611
|
+
};
|
|
2612
|
+
|
|
2613
|
+
/**
|
|
2614
|
+
* Strict-prefix delta for stateful `previous_response_id` chaining (used by the
|
|
2615
|
+
* platform Responses provider and the Codex provider on both transports):
|
|
2616
|
+
* returns the input items the current request appends beyond the previous
|
|
2617
|
+
* request's input plus the previous response's output items, or null when the
|
|
2618
|
+
* request options differ or history mutated (the chain must break). Per-turn
|
|
2619
|
+
* `client_metadata` (e.g. rotating turn ids) is excluded from the option
|
|
2620
|
+
* comparison; codex-rs excludes it from the same check.
|
|
2621
|
+
*/
|
|
2622
|
+
export function buildResponsesDeltaInput<TItem extends ResponseInputItem | InputItem>(
|
|
2623
|
+
previous: { input?: TItem[] } | undefined,
|
|
2624
|
+
previousResponseItems: readonly TItem[] | undefined,
|
|
2625
|
+
current: { input?: TItem[] },
|
|
2626
|
+
): TItem[] | null {
|
|
2627
|
+
if (!previous) return null;
|
|
2628
|
+
if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
|
|
2629
|
+
if (!deepEqualsWithout(previous, current, TOP_LEVEL_EXCLUDE_MAP)) {
|
|
2630
|
+
return null;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
const baselineLen = (previous.input?.length ?? 0) + (previousResponseItems?.length ?? 0);
|
|
2634
|
+
if (current.input.length <= baselineLen) return null;
|
|
2635
|
+
|
|
2636
|
+
let index = 0;
|
|
2637
|
+
for (const series of [previous.input, previousResponseItems]) {
|
|
2638
|
+
if (!series) continue;
|
|
2639
|
+
for (const item of series) {
|
|
2640
|
+
if (deepEqualsWithout(item, current.input[index])) {
|
|
2641
|
+
index++;
|
|
2642
|
+
} else {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return current.input.slice(index) as TItem[];
|
|
2648
|
+
}
|