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,2173 @@
|
|
|
1
|
+
import type { Effort } from "jeopi-catalog/effort";
|
|
2
|
+
import { isKimiModelId } from "jeopi-catalog/identity";
|
|
3
|
+
import { resolveWireModelId } from "jeopi-catalog/model-thinking";
|
|
4
|
+
import { calculateCost } from "jeopi-catalog/models";
|
|
5
|
+
import type { ResolvedOpenAICompat } from "jeopi-catalog/types";
|
|
6
|
+
import { $env, parseStreamingJson, parseStreamingJsonThrottled } from "jeopi-utils";
|
|
7
|
+
import { renderDemotedThinking } from "../dialect/demotion";
|
|
8
|
+
import * as AIError from "../error";
|
|
9
|
+
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
|
|
10
|
+
import { getEnvApiKey } from "../stream";
|
|
11
|
+
import type {
|
|
12
|
+
AssistantMessage,
|
|
13
|
+
Context,
|
|
14
|
+
Message,
|
|
15
|
+
MessageAttribution,
|
|
16
|
+
Model,
|
|
17
|
+
ProviderSessionState,
|
|
18
|
+
RawSseEvent,
|
|
19
|
+
ServiceTier,
|
|
20
|
+
StopReason,
|
|
21
|
+
StreamFunction,
|
|
22
|
+
StreamOptions,
|
|
23
|
+
TextContent,
|
|
24
|
+
ThinkingContent,
|
|
25
|
+
Tool,
|
|
26
|
+
ToolCall,
|
|
27
|
+
ToolChoice,
|
|
28
|
+
ToolResultMessage,
|
|
29
|
+
} from "../types";
|
|
30
|
+
import { normalizeSystemPrompts } from "../utils";
|
|
31
|
+
import { createAbortSourceTracker } from "../utils/abort";
|
|
32
|
+
import { kStreamingLastParseLen } from "../utils/block-symbols";
|
|
33
|
+
import { hasVisibleAssistantContent, withEmptyCompletionRetry } from "../utils/empty-completion-retry";
|
|
34
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
35
|
+
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
36
|
+
import {
|
|
37
|
+
getOpenAIStreamFirstEventTimeoutMs,
|
|
38
|
+
getOpenAIStreamIdleTimeoutMs,
|
|
39
|
+
iterateWithIdleTimeout,
|
|
40
|
+
iterateWithTerminalGrace,
|
|
41
|
+
} from "../utils/idle-iterator";
|
|
42
|
+
import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
|
|
43
|
+
import { notifyProviderResponse } from "../utils/provider-response";
|
|
44
|
+
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
45
|
+
import { adaptSchemaForStrict, NO_STRICT, normalizeSchemaForMoonshot, toolWireSchema } from "../utils/schema";
|
|
46
|
+
import {
|
|
47
|
+
type HealedToolCall,
|
|
48
|
+
StreamMarkupHealing,
|
|
49
|
+
type StreamMarkupHealingEvent,
|
|
50
|
+
} from "../utils/stream-markup-healing";
|
|
51
|
+
import { isForcedToolChoice, mapToOpenAICompletionsToolChoice } from "../utils/tool-choice";
|
|
52
|
+
import type {
|
|
53
|
+
ChatCompletionAssistantMessageParam,
|
|
54
|
+
ChatCompletionChunk,
|
|
55
|
+
ChatCompletionContentPart,
|
|
56
|
+
ChatCompletionContentPartImage,
|
|
57
|
+
ChatCompletionContentPartText,
|
|
58
|
+
ChatCompletionMessageParam,
|
|
59
|
+
ChatCompletionTool,
|
|
60
|
+
ChatCompletionToolMessageParam,
|
|
61
|
+
} from "./openai-chat-wire";
|
|
62
|
+
import {
|
|
63
|
+
applyOpenAIReasoningEffortFallback,
|
|
64
|
+
clearOpenAIReasoningEffortFallbackState,
|
|
65
|
+
createOpenAIReasoningEffortFallbackKey,
|
|
66
|
+
createOpenAIReasoningEffortFallbackState,
|
|
67
|
+
getOpenAIReasoningEffortFallback,
|
|
68
|
+
type OpenAIReasoningEffortFallback,
|
|
69
|
+
type OpenAIReasoningEffortFallbackState,
|
|
70
|
+
rememberOpenAIReasoningEffortFallback,
|
|
71
|
+
resolveOpenAIReasoningEffortFallback,
|
|
72
|
+
} from "./openai-reasoning-fallback";
|
|
73
|
+
import {
|
|
74
|
+
applyChatCompletionsCompatPolicy,
|
|
75
|
+
applyChatCompletionsToolStream,
|
|
76
|
+
applyOpenAIExtraBody,
|
|
77
|
+
applyOpenAIGatewayRouting,
|
|
78
|
+
applyOpenAIServiceTier,
|
|
79
|
+
applyWireModelIdTransform,
|
|
80
|
+
calculateOpenAIUsageAccounting,
|
|
81
|
+
clearOpenAIStrictToolsState,
|
|
82
|
+
createInitialResponsesAssistantMessage,
|
|
83
|
+
createOpenAIStrictToolsState,
|
|
84
|
+
disableStrictToolsForScope,
|
|
85
|
+
getOpenAIStrictToolsScope,
|
|
86
|
+
isCompiledGrammarTooLargeStrictError,
|
|
87
|
+
isOpenRouterAnthropicModel,
|
|
88
|
+
isStrictToolsDisabledForScope,
|
|
89
|
+
type OpenAICompatPolicy,
|
|
90
|
+
type OpenAICompletionsParams,
|
|
91
|
+
type OpenAIRequestSetup,
|
|
92
|
+
type OpenAIStrictToolsState,
|
|
93
|
+
parseAzureDeploymentNameMap,
|
|
94
|
+
resolveOpenAICompatPolicy,
|
|
95
|
+
resolveOpenAIOutputTokenParam,
|
|
96
|
+
resolveOpenAIRequestSetup,
|
|
97
|
+
resolveZaiReasoningOutputClamp,
|
|
98
|
+
shouldRetryWithoutStrictTools,
|
|
99
|
+
} from "./openai-shared";
|
|
100
|
+
import { transformMessages } from "./transform-messages";
|
|
101
|
+
import {
|
|
102
|
+
isDashscopeCompatibleModeTextOnlyQwen,
|
|
103
|
+
joinTextWithImagePlaceholder,
|
|
104
|
+
NON_VISION_IMAGE_PLACEHOLDER,
|
|
105
|
+
} from "./vision-guard";
|
|
106
|
+
|
|
107
|
+
export { applyOpenRouterRoutingVariant } from "./openai-shared";
|
|
108
|
+
|
|
109
|
+
type OpenAICompletionsReasoningField = NonNullable<ResolvedOpenAICompat["reasoningContentField"]>;
|
|
110
|
+
|
|
111
|
+
type ProviderAttributedChatCompletionChunk = ChatCompletionChunk & {
|
|
112
|
+
provider?: unknown;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type OpenAICompletionsChoiceUsage = ChatCompletionChunk.Choice & {
|
|
116
|
+
usage?: unknown;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type OpenAICompletionsDeltaWithReasoningDetails = ChatCompletionChunk.Choice["delta"] & {
|
|
120
|
+
reasoning_details?: unknown;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type OpenAICompletionsAssistantMessageParam = ChatCompletionAssistantMessageParam &
|
|
124
|
+
Partial<Record<OpenAICompletionsReasoningField, string>> & {
|
|
125
|
+
reasoning_details?: unknown[];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type OpenAICompletionsToolMessageParam = ChatCompletionToolMessageParam & {
|
|
129
|
+
name?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
type OpenAICompletionsUsageLike = {
|
|
133
|
+
completion_tokens?: unknown;
|
|
134
|
+
prompt_tokens?: unknown;
|
|
135
|
+
cached_tokens?: unknown;
|
|
136
|
+
prompt_cache_hit_tokens?: unknown;
|
|
137
|
+
prompt_cache_miss_tokens?: unknown;
|
|
138
|
+
prompt_tokens_details?: unknown;
|
|
139
|
+
completion_tokens_details?: unknown;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type OpenAICompletionsPromptTokenDetails = {
|
|
143
|
+
cached_tokens?: unknown;
|
|
144
|
+
cache_write_tokens?: unknown;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type OpenAICompletionsCompletionTokenDetails = {
|
|
148
|
+
reasoning_tokens?: unknown;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalize tool call ID for Mistral.
|
|
153
|
+
* Mistral requires tool IDs to be exactly 9 alphanumeric characters (a-z, A-Z, 0-9).
|
|
154
|
+
*/
|
|
155
|
+
function normalizeMistralToolId(id: string, isMistral: boolean): string {
|
|
156
|
+
if (!isMistral) return id;
|
|
157
|
+
// Remove non-alphanumeric characters
|
|
158
|
+
let normalized = id.replace(/[^a-zA-Z0-9]/g, "");
|
|
159
|
+
// Mistral requires exactly 9 characters
|
|
160
|
+
if (normalized.length < 9) {
|
|
161
|
+
// Pad with deterministic characters based on original ID to ensure matching
|
|
162
|
+
const padding = "ABCDEFGHI";
|
|
163
|
+
normalized = normalized + padding.slice(0, 9 - normalized.length);
|
|
164
|
+
} else if (normalized.length > 9) {
|
|
165
|
+
normalized = normalized.slice(0, 9);
|
|
166
|
+
}
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
// Direct DeepSeek model ids on NanoGPT are routed via the default tools-capable
|
|
170
|
+
// path. We deliberately do NOT append `:tools` here: with `:tools`, NanoGPT
|
|
171
|
+
// performs server-side tool-call parsing on the upstream DeepSeek stream and
|
|
172
|
+
// 502s with `code: "malformed_tool_call"` on more complex tool schemas (issue
|
|
173
|
+
// #1488). The default route forwards `delta.content` (including DSML
|
|
174
|
+
// envelope leaks) which `StreamMarkupHealing` heals into a structured call
|
|
175
|
+
// client-side.
|
|
176
|
+
function resolveOpenAICompletionsRoutingEffort(
|
|
177
|
+
model: Model<"openai-completions">,
|
|
178
|
+
effort: Effort | undefined,
|
|
179
|
+
): Effort | undefined {
|
|
180
|
+
if (!effort) return undefined;
|
|
181
|
+
if (model.thinking?.efforts.includes(effort)) return effort;
|
|
182
|
+
const compatMappedEffort = model.compat.reasoningEffortMap?.[effort] as Effort | undefined;
|
|
183
|
+
if (compatMappedEffort && model.thinking?.efforts.includes(compatMappedEffort)) return compatMappedEffort;
|
|
184
|
+
const thinkingMappedEffort = model.thinking?.effortMap?.[effort] as Effort | undefined;
|
|
185
|
+
if (thinkingMappedEffort && model.thinking?.efforts.includes(thinkingMappedEffort)) return thinkingMappedEffort;
|
|
186
|
+
return effort;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveOpenAICompletionsModelId(
|
|
190
|
+
model: Model<"openai-completions">,
|
|
191
|
+
options: OpenAICompletionsOptions | undefined,
|
|
192
|
+
): string {
|
|
193
|
+
// Effort-tier variants route per request effort (off → bare id, efforts →
|
|
194
|
+
// the thinking backing id); catalog variants (Copilot long-context `-1m`
|
|
195
|
+
// entries) pin via `requestModelId`; everything else serializes `model.id`.
|
|
196
|
+
const requestedEffort =
|
|
197
|
+
options?.reasoning && !options.disableReasoning && model.reasoning ? (options.reasoning as Effort) : undefined;
|
|
198
|
+
const effort = resolveOpenAICompletionsRoutingEffort(model, requestedEffort);
|
|
199
|
+
const wireId = resolveWireModelId(model, effort);
|
|
200
|
+
return applyWireModelIdTransform(wireId, model.compat.wireModelIdMode, options?.openrouterVariant);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Normalize OpenAI-compatible streaming `delta.content` into plain text.
|
|
205
|
+
* Most providers stream `delta.content` as a string, but some (notably Mistral
|
|
206
|
+
* Medium 3.5 / `mistral-medium-2604`) return an array of typed content parts
|
|
207
|
+
* — e.g. `[{ type: "text", text: "Hello" }]`. Without normalization those
|
|
208
|
+
* parts get string-coerced via `text += array`, producing the literal
|
|
209
|
+
* `[object Object]` sequences observed in issue #911.
|
|
210
|
+
*
|
|
211
|
+
* Returns the joined text. Non-text parts and unknown shapes are skipped so
|
|
212
|
+
* we never emit JS object sigils as visible output.
|
|
213
|
+
*/
|
|
214
|
+
function normalizeStreamingContentText(content: unknown): string {
|
|
215
|
+
if (typeof content === "string") return content;
|
|
216
|
+
if (Array.isArray(content)) {
|
|
217
|
+
let out = "";
|
|
218
|
+
for (const part of content) {
|
|
219
|
+
if (typeof part === "string") {
|
|
220
|
+
out += part;
|
|
221
|
+
} else if (part && typeof part === "object") {
|
|
222
|
+
const obj = part as { type?: unknown; text?: unknown };
|
|
223
|
+
if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
|
|
224
|
+
out += obj.text;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
if (content && typeof content === "object") {
|
|
231
|
+
const obj = content as { type?: unknown; text?: unknown };
|
|
232
|
+
if ((obj.type === undefined || obj.type === "text") && typeof obj.text === "string") {
|
|
233
|
+
return obj.text;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function serializeToolArguments(value: unknown): string {
|
|
240
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
241
|
+
try {
|
|
242
|
+
return JSON.stringify(value);
|
|
243
|
+
} catch {
|
|
244
|
+
return "{}";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof value === "string") {
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
if (trimmed.length === 0) return "{}";
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(trimmed);
|
|
253
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
254
|
+
return JSON.stringify(parsed);
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
return "{}";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return "{}";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function cloneStreamingArgumentValue(value: unknown): unknown {
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
return value.map(cloneStreamingArgumentValue);
|
|
266
|
+
}
|
|
267
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
268
|
+
return mergeStreamingArgumentObjects(undefined, value as Record<string, unknown>);
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function streamingArgumentValuesEqual(left: unknown, right: unknown): boolean {
|
|
274
|
+
if (left === right) return true;
|
|
275
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
276
|
+
if (left.length !== right.length) return false;
|
|
277
|
+
for (let i = 0; i < left.length; i++) {
|
|
278
|
+
if (!streamingArgumentValuesEqual(left[i], right[i])) return false;
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
left !== null &&
|
|
284
|
+
typeof left === "object" &&
|
|
285
|
+
!Array.isArray(left) &&
|
|
286
|
+
right !== null &&
|
|
287
|
+
typeof right === "object" &&
|
|
288
|
+
!Array.isArray(right)
|
|
289
|
+
) {
|
|
290
|
+
const leftObject = left as Record<string, unknown>;
|
|
291
|
+
const rightObject = right as Record<string, unknown>;
|
|
292
|
+
let leftKeys = 0;
|
|
293
|
+
for (const key in leftObject) {
|
|
294
|
+
if (!Object.hasOwn(leftObject, key) || key === "__proto__" || key === "constructor" || key === "prototype")
|
|
295
|
+
continue;
|
|
296
|
+
leftKeys++;
|
|
297
|
+
if (!Object.hasOwn(rightObject, key) || !streamingArgumentValuesEqual(leftObject[key], rightObject[key])) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let rightKeys = 0;
|
|
302
|
+
for (const key in rightObject) {
|
|
303
|
+
if (!Object.hasOwn(rightObject, key) || key === "__proto__" || key === "constructor" || key === "prototype")
|
|
304
|
+
continue;
|
|
305
|
+
rightKeys++;
|
|
306
|
+
}
|
|
307
|
+
return leftKeys === rightKeys;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function streamingArgumentArrayStartsWith(value: unknown[], prefix: unknown[]): boolean {
|
|
313
|
+
if (prefix.length > value.length) return false;
|
|
314
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
315
|
+
if (!streamingArgumentValuesEqual(value[i], prefix[i])) return false;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function mergeStreamingArgumentArrays(prev: unknown[], fragment: unknown[]): unknown[] {
|
|
321
|
+
if (streamingArgumentArrayStartsWith(fragment, prev)) {
|
|
322
|
+
return fragment.map(cloneStreamingArgumentValue);
|
|
323
|
+
}
|
|
324
|
+
if (streamingArgumentArrayStartsWith(prev, fragment)) {
|
|
325
|
+
return prev.map(cloneStreamingArgumentValue);
|
|
326
|
+
}
|
|
327
|
+
const merged = prev.map(cloneStreamingArgumentValue);
|
|
328
|
+
for (const value of fragment) {
|
|
329
|
+
merged.push(cloneStreamingArgumentValue(value));
|
|
330
|
+
}
|
|
331
|
+
return merged;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function mergeStreamingArgumentValues(prev: unknown, fragment: unknown): unknown {
|
|
335
|
+
if (typeof prev === "string" && typeof fragment === "string") {
|
|
336
|
+
return fragment.startsWith(prev) ? fragment : prev + fragment;
|
|
337
|
+
}
|
|
338
|
+
if (Array.isArray(prev) && Array.isArray(fragment)) {
|
|
339
|
+
return mergeStreamingArgumentArrays(prev, fragment);
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
prev !== null &&
|
|
343
|
+
typeof prev === "object" &&
|
|
344
|
+
!Array.isArray(prev) &&
|
|
345
|
+
fragment !== null &&
|
|
346
|
+
typeof fragment === "object" &&
|
|
347
|
+
!Array.isArray(fragment)
|
|
348
|
+
) {
|
|
349
|
+
return mergeStreamingArgumentObjects(prev as Record<string, unknown>, fragment as Record<string, unknown>);
|
|
350
|
+
}
|
|
351
|
+
return cloneStreamingArgumentValue(fragment);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function mergeStreamingArgumentObjects(
|
|
355
|
+
prev: Record<string, unknown> | undefined,
|
|
356
|
+
fragment: Record<string, unknown>,
|
|
357
|
+
): Record<string, unknown> {
|
|
358
|
+
const merged: Record<string, unknown> = {};
|
|
359
|
+
if (prev) {
|
|
360
|
+
for (const key in prev) {
|
|
361
|
+
if (!Object.hasOwn(prev, key) || key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
362
|
+
merged[key] = cloneStreamingArgumentValue(prev[key]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const key in fragment) {
|
|
366
|
+
if (!Object.hasOwn(fragment, key) || key === "__proto__" || key === "constructor" || key === "prototype")
|
|
367
|
+
continue;
|
|
368
|
+
merged[key] = Object.hasOwn(merged, key)
|
|
369
|
+
? mergeStreamingArgumentValues(merged[key], fragment[key])
|
|
370
|
+
: cloneStreamingArgumentValue(fragment[key]);
|
|
371
|
+
}
|
|
372
|
+
return merged;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if conversation messages contain tool calls or tool results.
|
|
377
|
+
* This is needed because Anthropic (via proxy) requires the tools param
|
|
378
|
+
* to be present when messages include tool_calls or tool role messages.
|
|
379
|
+
*/
|
|
380
|
+
function hasToolHistory(messages: Message[]): boolean {
|
|
381
|
+
for (const msg of messages) {
|
|
382
|
+
if (msg.role === "toolResult") {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
if (msg.role === "assistant") {
|
|
386
|
+
if (msg.content.some(block => block.type === "toolCall")) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Identify "real progress" stream chunks vs. keepalives, role-only preambles,
|
|
395
|
+
* and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
|
|
396
|
+
* Without this filter, every keepalive resets `iterateWithIdleTimeout`'s
|
|
397
|
+
* deadline, so a provider that streams nothing but pings keeps the watchdog
|
|
398
|
+
* asleep indefinitely — observed against z.ai/GLM via OpenRouter where a
|
|
399
|
+
* subagent stalled for hours with no error surfaced.
|
|
400
|
+
*
|
|
401
|
+
* A chunk counts as progress when it carries terminal usage, a finish reason,
|
|
402
|
+
* or a model-produced delta (content / tool calls / reasoning / refusal).
|
|
403
|
+
* Role-only `delta: { role: "assistant" }` preambles do NOT count; we want the
|
|
404
|
+
* (longer) first-event timeout to keep governing until real output appears.
|
|
405
|
+
*/
|
|
406
|
+
export function isOpenAICompletionsProgressChunk(chunk: unknown): boolean {
|
|
407
|
+
if (!chunk || typeof chunk !== "object") return false;
|
|
408
|
+
const record = chunk as {
|
|
409
|
+
usage?: unknown;
|
|
410
|
+
choices?: ReadonlyArray<{
|
|
411
|
+
finish_reason?: unknown;
|
|
412
|
+
usage?: unknown;
|
|
413
|
+
delta?: {
|
|
414
|
+
content?: unknown;
|
|
415
|
+
tool_calls?: unknown;
|
|
416
|
+
reasoning?: unknown;
|
|
417
|
+
reasoning_content?: unknown;
|
|
418
|
+
reasoning_text?: unknown;
|
|
419
|
+
refusal?: unknown;
|
|
420
|
+
};
|
|
421
|
+
}>;
|
|
422
|
+
};
|
|
423
|
+
if (record.usage) return true;
|
|
424
|
+
const choice = Array.isArray(record.choices) ? record.choices[0] : undefined;
|
|
425
|
+
if (!choice) return false;
|
|
426
|
+
if (choice.finish_reason) return true;
|
|
427
|
+
if (choice.usage) return true;
|
|
428
|
+
const delta = choice.delta;
|
|
429
|
+
if (!delta) return false;
|
|
430
|
+
const content = delta.content;
|
|
431
|
+
if (typeof content === "string" ? content.length > 0 : Array.isArray(content) && content.length > 0) return true;
|
|
432
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
|
|
433
|
+
if (typeof delta.reasoning === "string" && delta.reasoning.length > 0) return true;
|
|
434
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) return true;
|
|
435
|
+
if (typeof delta.reasoning_text === "string" && delta.reasoning_text.length > 0) return true;
|
|
436
|
+
if (typeof delta.refusal === "string" && delta.refusal.length > 0) return true;
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export interface OpenAICompletionsOptions extends StreamOptions {
|
|
441
|
+
toolChoice?: ToolChoice;
|
|
442
|
+
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
443
|
+
/** Force-disable reasoning where supported, or request the lowest effort on generic effort endpoints. */
|
|
444
|
+
disableReasoning?: boolean;
|
|
445
|
+
serviceTier?: ServiceTier;
|
|
446
|
+
/** @internal True when maxTokens came from the caller, not the model default. */
|
|
447
|
+
maxTokensExplicit?: boolean;
|
|
448
|
+
/**
|
|
449
|
+
* Routing-variant suffix appended to OpenRouter model IDs when none is
|
|
450
|
+
* already present (`anthropic/claude-haiku-latest` → `…:nitro`). Common
|
|
451
|
+
* values: `"nitro"`, `"floor"`, `"online"`, `"exacto"`. Ignored when the
|
|
452
|
+
* resolved `model.id` already contains a colon-suffix after the last
|
|
453
|
+
* provider segment (explicit `:nitro` in the selector or a catalog entry
|
|
454
|
+
* with the variant baked in).
|
|
455
|
+
*/
|
|
456
|
+
openrouterVariant?: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
type AppliedToolStrictMode = "mixed" | "all_strict" | "none";
|
|
460
|
+
type ToolStrictModeOverride = Exclude<ResolvedOpenAICompat["toolStrictMode"], "mixed"> | undefined;
|
|
461
|
+
|
|
462
|
+
type BuiltOpenAICompletionTools = {
|
|
463
|
+
tools: ChatCompletionTool[];
|
|
464
|
+
toolStrictMode: AppliedToolStrictMode;
|
|
465
|
+
/** True when at least one wire tool was sent with `strict: true`. */
|
|
466
|
+
strictToolsApplied: boolean;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const OPENAI_COMPLETIONS_PROVIDER_SESSION_STATE_PREFIX = "openai-completions:";
|
|
470
|
+
|
|
471
|
+
type OpenAICompletionsProviderSessionState = ProviderSessionState &
|
|
472
|
+
OpenAIStrictToolsState &
|
|
473
|
+
OpenAIReasoningEffortFallbackState;
|
|
474
|
+
|
|
475
|
+
function createOpenAICompletionsProviderSessionState(): OpenAICompletionsProviderSessionState {
|
|
476
|
+
const strictToolsState = createOpenAIStrictToolsState();
|
|
477
|
+
const reasoningEffortFallbackState = createOpenAIReasoningEffortFallbackState();
|
|
478
|
+
const state: OpenAICompletionsProviderSessionState = {
|
|
479
|
+
...strictToolsState,
|
|
480
|
+
...reasoningEffortFallbackState,
|
|
481
|
+
close: () => {
|
|
482
|
+
clearOpenAIStrictToolsState(state);
|
|
483
|
+
clearOpenAIReasoningEffortFallbackState(state);
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
return state;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getOpenAICompletionsProviderSessionState(
|
|
490
|
+
model: Model<"openai-completions">,
|
|
491
|
+
baseUrl: string | undefined,
|
|
492
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
493
|
+
): OpenAICompletionsProviderSessionState | undefined {
|
|
494
|
+
if (!providerSessionState) return undefined;
|
|
495
|
+
const key = `${OPENAI_COMPLETIONS_PROVIDER_SESSION_STATE_PREFIX}${model.provider}:${baseUrl ?? ""}:${model.id}`;
|
|
496
|
+
const existing = providerSessionState.get(key) as OpenAICompletionsProviderSessionState | undefined;
|
|
497
|
+
if (existing) return existing;
|
|
498
|
+
const created = createOpenAICompletionsProviderSessionState();
|
|
499
|
+
providerSessionState.set(key, created);
|
|
500
|
+
return created;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// DeepSeek models leak chat-template special tokens (e.g. `<|tool_calls_begin|>`,
|
|
504
|
+
// `<|DSML|tool_calls|>`) into visible `content` deltas when hosted behind providers
|
|
505
|
+
// (such as NVIDIA NIM) that don't strip them server-side. The structured `tool_calls`
|
|
506
|
+
// payload is still emitted correctly — we only need to filter the leaked markers from
|
|
507
|
+
// user-visible text. Tokens use either fullwidth pipes (|, U+FF5C) or ASCII pipes.
|
|
508
|
+
// Body is restricted to identifier-like chars (with the DeepSeek tokenizer's `▁`),
|
|
509
|
+
// capped at a sane length to avoid swallowing legitimate angle-bracket text.
|
|
510
|
+
const DEEPSEEK_SPECIAL_TOKEN_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/g;
|
|
511
|
+
const DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX = /^\s*<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/;
|
|
512
|
+
const DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>\s*$/;
|
|
513
|
+
const DEEPSEEK_OPEN_DELIMS = ["<|", "<|"] as const;
|
|
514
|
+
|
|
515
|
+
function stripDeepseekSpecialTokens(text: string): string {
|
|
516
|
+
const stripped = text.replace(DEEPSEEK_SPECIAL_TOKEN_REGEX, "");
|
|
517
|
+
if (stripped === text) return text;
|
|
518
|
+
|
|
519
|
+
let normalized = stripped;
|
|
520
|
+
if (DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX.test(text)) normalized = normalized.replace(/^\s+/u, "");
|
|
521
|
+
if (DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX.test(text)) normalized = normalized.replace(/\s+$/u, "");
|
|
522
|
+
return normalized;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Find a trailing partial `<|...` (or `<|...`) that has not yet been closed by a
|
|
526
|
+
// matching `|>`/`|>`, so it can be held back until the next chunk arrives. A solo
|
|
527
|
+
// trailing `<` is also held in case it is the start of a new token.
|
|
528
|
+
function getTrailingPartialDeepseekToken(text: string): string {
|
|
529
|
+
let bestIdx = -1;
|
|
530
|
+
for (const delim of DEEPSEEK_OPEN_DELIMS) {
|
|
531
|
+
const idx = text.lastIndexOf(delim);
|
|
532
|
+
if (idx > bestIdx) bestIdx = idx;
|
|
533
|
+
}
|
|
534
|
+
if (bestIdx === -1) {
|
|
535
|
+
return text.endsWith("<") ? "<" : "";
|
|
536
|
+
}
|
|
537
|
+
const tail = text.slice(bestIdx);
|
|
538
|
+
if (tail.includes("|>") || tail.includes("|>")) return "";
|
|
539
|
+
// Cap the held-back length so a stray `<|` in normal prose can't grow unboundedly.
|
|
540
|
+
if (tail.length > 256) return "";
|
|
541
|
+
return tail;
|
|
542
|
+
}
|
|
543
|
+
const OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE =
|
|
544
|
+
"OpenAI completions stream timed out while waiting for the first event";
|
|
545
|
+
// How long to keep draining the stream after a `finish_reason` chunk arrived.
|
|
546
|
+
// Compliant hosts follow it (almost) immediately with an optional usage-only
|
|
547
|
+
// chunk and the `[DONE]` sentinel, so the window only ever elapses on hosts
|
|
548
|
+
// that hold the connection open after the response logically completed —
|
|
549
|
+
// without it the turn parks on `iterator.next()` until the idle watchdog
|
|
550
|
+
// converts the already-successful response into a timeout error.
|
|
551
|
+
const OPENAI_COMPLETIONS_POST_FINISH_GRACE_MS = 2_500;
|
|
552
|
+
|
|
553
|
+
const streamOpenAICompletionsOnce = (
|
|
554
|
+
model: Model<"openai-completions">,
|
|
555
|
+
context: Context,
|
|
556
|
+
options?: OpenAICompletionsOptions,
|
|
557
|
+
): AssistantMessageEventStream => {
|
|
558
|
+
const stream = new AssistantMessageEventStream();
|
|
559
|
+
|
|
560
|
+
(async () => {
|
|
561
|
+
const startTime = performance.now();
|
|
562
|
+
let firstTokenTime: number | undefined;
|
|
563
|
+
const policy = resolveOpenAICompatForRequest(model, options);
|
|
564
|
+
|
|
565
|
+
const output: AssistantMessage = createInitialResponsesAssistantMessage(model.api, model.provider, model.id);
|
|
566
|
+
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
567
|
+
const abortTracker = createAbortSourceTracker(options?.signal);
|
|
568
|
+
const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
|
|
569
|
+
OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
570
|
+
);
|
|
571
|
+
const { requestAbortController, requestSignal } = abortTracker;
|
|
572
|
+
const onSseEvent = options?.onSseEvent;
|
|
573
|
+
const rawSseObserver = onSseEvent
|
|
574
|
+
? (event: RawSseEvent) => {
|
|
575
|
+
if (!event.event && event.data && event.data !== "[DONE]") {
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(event.data);
|
|
578
|
+
const resolvedEvent =
|
|
579
|
+
typeof parsed.type === "string"
|
|
580
|
+
? parsed.type
|
|
581
|
+
: typeof parsed.object === "string"
|
|
582
|
+
? parsed.object
|
|
583
|
+
: null;
|
|
584
|
+
if (resolvedEvent) {
|
|
585
|
+
event.event = resolvedEvent;
|
|
586
|
+
event.raw = [`event: ${resolvedEvent}`, ...event.raw];
|
|
587
|
+
}
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
onSseEvent(event, model);
|
|
591
|
+
}
|
|
592
|
+
: undefined;
|
|
593
|
+
// Assigned once the block helpers exist (they are scoped to the `try`);
|
|
594
|
+
// the catch handler uses it to close open blocks before emitting the
|
|
595
|
+
// terminal error so both exit paths obey the same block lifecycle.
|
|
596
|
+
let finishOpenBlocksOnError: () => void = () => {};
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
600
|
+
const idleTimeoutFallbackMs = model.compat.streamIdleTimeoutMs;
|
|
601
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(idleTimeoutFallbackMs);
|
|
602
|
+
const firstEventTimeoutMs =
|
|
603
|
+
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
604
|
+
const requestTimeoutMs =
|
|
605
|
+
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
606
|
+
const { copilotPremiumRequests, baseUrl, headers, query, requestHeaders } = createRequestSetup(
|
|
607
|
+
model,
|
|
608
|
+
context,
|
|
609
|
+
apiKey,
|
|
610
|
+
options?.headers,
|
|
611
|
+
options?.initiatorOverride,
|
|
612
|
+
);
|
|
613
|
+
const premiumRequestsTotal = copilotPremiumRequests;
|
|
614
|
+
let appliedStrictTools = false;
|
|
615
|
+
const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
|
|
616
|
+
const attemptedReasoningEffortFallbacks = new Set<string>();
|
|
617
|
+
let activeReasoningEffortFallbackKey: string | undefined;
|
|
618
|
+
let activeRequestParams: OpenAICompletionsParams | undefined;
|
|
619
|
+
const providerSessionState = getOpenAICompletionsProviderSessionState(
|
|
620
|
+
model,
|
|
621
|
+
baseUrl,
|
|
622
|
+
options?.providerSessionState,
|
|
623
|
+
);
|
|
624
|
+
const strictToolsScope = getOpenAIStrictToolsScope(model, baseUrl);
|
|
625
|
+
let disableStrictTools = isStrictToolsDisabledForScope(providerSessionState, strictToolsScope);
|
|
626
|
+
const trimmedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
627
|
+
const completionsUrl = query
|
|
628
|
+
? `${trimmedBaseUrl}/chat/completions?${new URLSearchParams(query)}`
|
|
629
|
+
: `${trimmedBaseUrl}/chat/completions`;
|
|
630
|
+
const createCompletionsStream = async (toolStrictModeOverride?: ToolStrictModeOverride) => {
|
|
631
|
+
const effectiveToolStrictModeOverride = disableStrictTools ? "none" : toolStrictModeOverride;
|
|
632
|
+
const { params, strictToolsApplied } = buildParams(
|
|
633
|
+
model,
|
|
634
|
+
context,
|
|
635
|
+
options,
|
|
636
|
+
effectiveToolStrictModeOverride,
|
|
637
|
+
);
|
|
638
|
+
appliedStrictTools = strictToolsApplied;
|
|
639
|
+
const reasoningEffortFallbackKey = createOpenAIReasoningEffortFallbackKey(
|
|
640
|
+
"chat-completions",
|
|
641
|
+
trimmedBaseUrl,
|
|
642
|
+
params.model,
|
|
643
|
+
);
|
|
644
|
+
const requestReasoningEffortFallback = requestReasoningEffortFallbacks.has(reasoningEffortFallbackKey)
|
|
645
|
+
? requestReasoningEffortFallbacks.get(reasoningEffortFallbackKey)
|
|
646
|
+
: getOpenAIReasoningEffortFallback(providerSessionState, reasoningEffortFallbackKey);
|
|
647
|
+
if (requestReasoningEffortFallback !== undefined) {
|
|
648
|
+
applyOpenAIReasoningEffortFallback(params, requestReasoningEffortFallback);
|
|
649
|
+
}
|
|
650
|
+
activeReasoningEffortFallbackKey = reasoningEffortFallbackKey;
|
|
651
|
+
activeRequestParams = params;
|
|
652
|
+
options?.onPayload?.(params);
|
|
653
|
+
rawRequestDump = {
|
|
654
|
+
provider: model.provider,
|
|
655
|
+
api: output.api,
|
|
656
|
+
model: model.id,
|
|
657
|
+
method: "POST",
|
|
658
|
+
url: completionsUrl,
|
|
659
|
+
headers: requestHeaders,
|
|
660
|
+
body: params,
|
|
661
|
+
};
|
|
662
|
+
let requestTimeout: NodeJS.Timeout | undefined;
|
|
663
|
+
if (requestTimeoutMs !== undefined) {
|
|
664
|
+
requestTimeout = setTimeout(
|
|
665
|
+
() => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
666
|
+
requestTimeoutMs,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const headersWithTimeout = { ...headers };
|
|
671
|
+
if (requestTimeoutMs !== undefined) {
|
|
672
|
+
headersWithTimeout["X-Stainless-Timeout"] = Math.floor(requestTimeoutMs / 1000).toString();
|
|
673
|
+
}
|
|
674
|
+
const { events, response, requestId } = await postOpenAIStream<ChatCompletionChunk>({
|
|
675
|
+
url: completionsUrl,
|
|
676
|
+
headers: headersWithTimeout,
|
|
677
|
+
body: params,
|
|
678
|
+
signal: requestSignal,
|
|
679
|
+
fetch: options?.fetch,
|
|
680
|
+
// With a first-event watchdog armed, transport retries must
|
|
681
|
+
// not silently extend the deadline (old SDK `maxRetries: 0`).
|
|
682
|
+
maxAttempts: requestTimeoutMs === undefined ? undefined : 1,
|
|
683
|
+
onSseEvent: rawSseObserver,
|
|
684
|
+
});
|
|
685
|
+
await notifyProviderResponse(options, response, model, requestId);
|
|
686
|
+
return events;
|
|
687
|
+
} finally {
|
|
688
|
+
// Headers arrived (or the request failed); from here the
|
|
689
|
+
// first-event deadline is enforced by `iterateWithIdleTimeout`.
|
|
690
|
+
if (requestTimeout !== undefined) clearTimeout(requestTimeout);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
let openaiStream: AsyncIterable<ChatCompletionChunk>;
|
|
694
|
+
try {
|
|
695
|
+
openaiStream = await callWithCopilotModelRetry(() => createCompletionsStream(), {
|
|
696
|
+
provider: model.provider,
|
|
697
|
+
signal: requestSignal,
|
|
698
|
+
});
|
|
699
|
+
} catch (error) {
|
|
700
|
+
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
701
|
+
const reasoningEffortFallback =
|
|
702
|
+
activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
|
|
703
|
+
? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
|
|
704
|
+
explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
|
|
705
|
+
})
|
|
706
|
+
: undefined;
|
|
707
|
+
if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
|
|
708
|
+
const retryMarker = `${activeReasoningEffortFallbackKey}:${String(reasoningEffortFallback)}`;
|
|
709
|
+
if (attemptedReasoningEffortFallbacks.has(retryMarker)) throw error;
|
|
710
|
+
attemptedReasoningEffortFallbacks.add(retryMarker);
|
|
711
|
+
requestReasoningEffortFallbacks.set(activeReasoningEffortFallbackKey, reasoningEffortFallback);
|
|
712
|
+
openaiStream = await createCompletionsStream();
|
|
713
|
+
rememberOpenAIReasoningEffortFallback(
|
|
714
|
+
providerSessionState,
|
|
715
|
+
activeReasoningEffortFallbackKey,
|
|
716
|
+
reasoningEffortFallback,
|
|
717
|
+
);
|
|
718
|
+
} else if (
|
|
719
|
+
isOpenRouterAnthropicModel(model) &&
|
|
720
|
+
!disableStrictTools &&
|
|
721
|
+
isCompiledGrammarTooLargeStrictError(error, capturedErrorResponse)
|
|
722
|
+
) {
|
|
723
|
+
disableStrictToolsForScope(providerSessionState, strictToolsScope);
|
|
724
|
+
disableStrictTools = true;
|
|
725
|
+
openaiStream = await createCompletionsStream("none");
|
|
726
|
+
} else {
|
|
727
|
+
if (!shouldRetryWithoutStrictTools(error, capturedErrorResponse, appliedStrictTools, context.tools)) {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
// Remember the rejection for the rest of the session so every
|
|
731
|
+
// subsequent request doesn't pay a strict-400 + retry round-trip.
|
|
732
|
+
disableStrictToolsForScope(providerSessionState, strictToolsScope);
|
|
733
|
+
disableStrictTools = true;
|
|
734
|
+
openaiStream = await createCompletionsStream("none");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (premiumRequestsTotal !== undefined) {
|
|
738
|
+
output.usage.premiumRequests = premiumRequestsTotal;
|
|
739
|
+
}
|
|
740
|
+
stream.push({ type: "start", partial: output });
|
|
741
|
+
|
|
742
|
+
// Some OpenAI-compatible DeepSeek hosts (including NVIDIA NIM and DeepSeek's
|
|
743
|
+
// native API) leak chat-template tool-call markers in `delta.content` even
|
|
744
|
+
// though tool calls are also surfaced structurally. Strip the leaked markers
|
|
745
|
+
// so users don't see raw `<|...|>` tokens.
|
|
746
|
+
const stripDeepseekChatTemplateTokens = policy.stream.stripSpecialTokens === "deepseek";
|
|
747
|
+
type ToolCallStreamBlock = ToolCall & {
|
|
748
|
+
partialArgs?: string | Record<string, unknown>;
|
|
749
|
+
streamIndex?: number;
|
|
750
|
+
[kStreamingLastParseLen]?: number;
|
|
751
|
+
};
|
|
752
|
+
type OpenAIStreamBlock = TextContent | ThinkingContent | ToolCallStreamBlock;
|
|
753
|
+
const pendingToolCallBlocks: ToolCallStreamBlock[] = [];
|
|
754
|
+
const toolCallBlockByIndex = new Map<number, ToolCallStreamBlock>();
|
|
755
|
+
let currentBlock: OpenAIStreamBlock | undefined;
|
|
756
|
+
const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
|
|
757
|
+
if (!block) return Math.max(0, output.content.length - 1);
|
|
758
|
+
return output.content.indexOf(block);
|
|
759
|
+
};
|
|
760
|
+
const finishToolCallBlock = (block: ToolCallStreamBlock): void => {
|
|
761
|
+
if (block.partialArgs === undefined) return;
|
|
762
|
+
const contentIndex = blockIndex(block);
|
|
763
|
+
if (contentIndex < 0) return;
|
|
764
|
+
// Object-shaped `partialArgs` came from MiniMax-compatible hosts that stream
|
|
765
|
+
// `function.arguments` as an object. The per-chunk handler holds them with an
|
|
766
|
+
// empty wire delta (see the object branch below) because emitting each chunk's
|
|
767
|
+
// `JSON.stringify(rawArgs)` would feed concat-based downstream consumers
|
|
768
|
+
// (proxy.ts, openai-chat-server, openai-responses-server, anthropic-messages-server)
|
|
769
|
+
// an invalid concatenation like `{"input":"a"}{"input":"b"}`. Flush the final
|
|
770
|
+
// merged object as one concat-safe delta now so those consumers reconstruct the
|
|
771
|
+
// args correctly before observing `toolcall_end`.
|
|
772
|
+
if (typeof block.partialArgs === "object" && !Array.isArray(block.partialArgs)) {
|
|
773
|
+
const fullJson = JSON.stringify(block.partialArgs);
|
|
774
|
+
if (fullJson.length > 0 && fullJson !== "{}") {
|
|
775
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: fullJson, partial: output });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
block.arguments =
|
|
779
|
+
typeof block.partialArgs === "string" ? parseStreamingJson(block.partialArgs) : block.partialArgs;
|
|
780
|
+
delete block.partialArgs;
|
|
781
|
+
if (block.streamIndex !== undefined) {
|
|
782
|
+
toolCallBlockByIndex.delete(block.streamIndex);
|
|
783
|
+
delete block.streamIndex;
|
|
784
|
+
}
|
|
785
|
+
const pendingIndex = pendingToolCallBlocks.indexOf(block);
|
|
786
|
+
if (pendingIndex >= 0) pendingToolCallBlocks.splice(pendingIndex, 1);
|
|
787
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
788
|
+
};
|
|
789
|
+
const finishPendingToolCallBlocks = (): void => {
|
|
790
|
+
for (const block of [...pendingToolCallBlocks]) {
|
|
791
|
+
finishToolCallBlock(block);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
const finishCurrentBlock = (block: OpenAIStreamBlock | undefined): void => {
|
|
795
|
+
if (!block) return;
|
|
796
|
+
const contentIndex = blockIndex(block);
|
|
797
|
+
if (contentIndex < 0) return;
|
|
798
|
+
if (block.type === "text") {
|
|
799
|
+
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (block.type === "thinking") {
|
|
803
|
+
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
finishToolCallBlock(block);
|
|
807
|
+
};
|
|
808
|
+
finishOpenBlocksOnError = () => {
|
|
809
|
+
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
810
|
+
finishPendingToolCallBlocks();
|
|
811
|
+
};
|
|
812
|
+
const appendText = (
|
|
813
|
+
message: AssistantMessage,
|
|
814
|
+
eventStream: AssistantMessageEventStream,
|
|
815
|
+
text: string,
|
|
816
|
+
): void => {
|
|
817
|
+
if (currentBlock?.type !== "text") {
|
|
818
|
+
// Leave toolCall blocks pending across text transitions: chunks after
|
|
819
|
+
// the first typically carry only `index`, so a finished (de-registered)
|
|
820
|
+
// call would be reborn as a nameless phantom block when its arguments
|
|
821
|
+
// resume. The stream-end sweep finalizes pending calls.
|
|
822
|
+
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
823
|
+
currentBlock = { type: "text", text: "" };
|
|
824
|
+
message.content.push(currentBlock);
|
|
825
|
+
eventStream.push({ type: "text_start", contentIndex: blockIndex(currentBlock), partial: message });
|
|
826
|
+
}
|
|
827
|
+
currentBlock.text += text;
|
|
828
|
+
eventStream.push({
|
|
829
|
+
type: "text_delta",
|
|
830
|
+
contentIndex: blockIndex(currentBlock),
|
|
831
|
+
delta: text,
|
|
832
|
+
partial: message,
|
|
833
|
+
});
|
|
834
|
+
};
|
|
835
|
+
const appendThinking = (
|
|
836
|
+
message: AssistantMessage,
|
|
837
|
+
eventStream: AssistantMessageEventStream,
|
|
838
|
+
thinking: string,
|
|
839
|
+
signature?: string,
|
|
840
|
+
): void => {
|
|
841
|
+
if (
|
|
842
|
+
currentBlock?.type !== "thinking" ||
|
|
843
|
+
(signature !== undefined && currentBlock.thinkingSignature !== signature)
|
|
844
|
+
) {
|
|
845
|
+
// Same as appendText: leave toolCall blocks pending so index-only
|
|
846
|
+
// continuation deltas can still find them.
|
|
847
|
+
if (currentBlock?.type !== "toolCall") finishCurrentBlock(currentBlock);
|
|
848
|
+
currentBlock = { type: "thinking", thinking: "", thinkingSignature: signature };
|
|
849
|
+
message.content.push(currentBlock);
|
|
850
|
+
eventStream.push({
|
|
851
|
+
type: "thinking_start",
|
|
852
|
+
contentIndex: blockIndex(currentBlock),
|
|
853
|
+
partial: message,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
if (signature !== undefined && !currentBlock.thinkingSignature) {
|
|
857
|
+
currentBlock.thinkingSignature = signature;
|
|
858
|
+
}
|
|
859
|
+
currentBlock.thinking += thinking;
|
|
860
|
+
eventStream.push({
|
|
861
|
+
type: "thinking_delta",
|
|
862
|
+
contentIndex: blockIndex(currentBlock),
|
|
863
|
+
delta: thinking,
|
|
864
|
+
partial: message,
|
|
865
|
+
});
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const appendTextDelta = (text: string): void => {
|
|
869
|
+
if (!text) return;
|
|
870
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
871
|
+
appendText(output, stream, text);
|
|
872
|
+
};
|
|
873
|
+
// Tracks the last full cumulative reasoning snapshot per signature (the
|
|
874
|
+
// reasoning field name) so dedup survives block transitions. Required
|
|
875
|
+
// for MiniMax-M3: once `</think>` and visible text arrive, currentBlock
|
|
876
|
+
// flips to "text", but later chunks keep carrying the same cumulative
|
|
877
|
+
// `reasoning_content` snapshot. Without an external tracker the guard
|
|
878
|
+
// below misses and the snapshot gets re-emitted as a fresh thinking
|
|
879
|
+
// block after the answer has started.
|
|
880
|
+
const lastCumulativeReasoningBySignature = new Map<string, string>();
|
|
881
|
+
const appendThinkingDelta = (
|
|
882
|
+
thinking: string,
|
|
883
|
+
signature?: string,
|
|
884
|
+
source: "delta" | "cumulative" = "delta",
|
|
885
|
+
): void => {
|
|
886
|
+
if (!thinking) return;
|
|
887
|
+
let emittedThinking = thinking;
|
|
888
|
+
if (source === "cumulative") {
|
|
889
|
+
const key = signature ?? "";
|
|
890
|
+
const lastSnapshot = lastCumulativeReasoningBySignature.get(key) ?? "";
|
|
891
|
+
if (thinking.startsWith(lastSnapshot)) {
|
|
892
|
+
emittedThinking = thinking.slice(lastSnapshot.length);
|
|
893
|
+
}
|
|
894
|
+
lastCumulativeReasoningBySignature.set(key, thinking);
|
|
895
|
+
if (!emittedThinking) return;
|
|
896
|
+
}
|
|
897
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
898
|
+
appendThinking(output, stream, emittedThinking, signature);
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
let deepseekStripBuffer = "";
|
|
902
|
+
const flushDeepseekStripBuffer = (final: boolean): void => {
|
|
903
|
+
if (deepseekStripBuffer.length === 0) return;
|
|
904
|
+
let flushable: string;
|
|
905
|
+
if (final) {
|
|
906
|
+
flushable = deepseekStripBuffer;
|
|
907
|
+
deepseekStripBuffer = "";
|
|
908
|
+
} else {
|
|
909
|
+
const trailing = getTrailingPartialDeepseekToken(deepseekStripBuffer);
|
|
910
|
+
flushable = deepseekStripBuffer.slice(0, deepseekStripBuffer.length - trailing.length);
|
|
911
|
+
deepseekStripBuffer = trailing;
|
|
912
|
+
}
|
|
913
|
+
const stripped = stripDeepseekSpecialTokens(flushable);
|
|
914
|
+
if (stripped && (stripped === flushable || stripped.trim().length > 0)) appendTextDelta(stripped);
|
|
915
|
+
};
|
|
916
|
+
const appendProcessedText = (processedText: string): void => {
|
|
917
|
+
if (processedText.length === 0) return;
|
|
918
|
+
if (stripDeepseekChatTemplateTokens) {
|
|
919
|
+
deepseekStripBuffer += processedText;
|
|
920
|
+
flushDeepseekStripBuffer(false);
|
|
921
|
+
} else {
|
|
922
|
+
appendTextDelta(processedText);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
const streamMarkupHealingPattern = policy.stream.markupHealingPattern;
|
|
926
|
+
const streamMarkupHealing = streamMarkupHealingPattern
|
|
927
|
+
? new StreamMarkupHealing({ pattern: streamMarkupHealingPattern })
|
|
928
|
+
: undefined;
|
|
929
|
+
const explicitReasoningDeltasMayBeCumulative = policy.stream.reasoningDeltasMayBeCumulative;
|
|
930
|
+
let suppressHealedThinking = false;
|
|
931
|
+
let healedToolCallEmitted = false;
|
|
932
|
+
const emitHealedToolCall = (call: HealedToolCall): void => {
|
|
933
|
+
finishCurrentBlock(currentBlock);
|
|
934
|
+
const block: ToolCall & { partialArgs: string } = {
|
|
935
|
+
type: "toolCall",
|
|
936
|
+
id: call.id,
|
|
937
|
+
name: call.name,
|
|
938
|
+
arguments: {},
|
|
939
|
+
partialArgs: call.arguments,
|
|
940
|
+
};
|
|
941
|
+
block.arguments = parseStreamingJson(call.arguments);
|
|
942
|
+
currentBlock = block;
|
|
943
|
+
output.content.push(block);
|
|
944
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(block), partial: output });
|
|
945
|
+
stream.push({
|
|
946
|
+
type: "toolcall_delta",
|
|
947
|
+
contentIndex: blockIndex(block),
|
|
948
|
+
delta: call.arguments,
|
|
949
|
+
partial: output,
|
|
950
|
+
});
|
|
951
|
+
finishCurrentBlock(block);
|
|
952
|
+
currentBlock = undefined;
|
|
953
|
+
healedToolCallEmitted = true;
|
|
954
|
+
};
|
|
955
|
+
const emitHealingEvent = (event: StreamMarkupHealingEvent, suppressThinking: boolean): void => {
|
|
956
|
+
if (event.type === "text") {
|
|
957
|
+
appendProcessedText(event.text);
|
|
958
|
+
} else if (event.type === "thinking") {
|
|
959
|
+
if (!suppressThinking) appendThinkingDelta(event.thinking);
|
|
960
|
+
} else {
|
|
961
|
+
emitHealedToolCall(event.call);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const flushHealedToolCalls = (): void => {
|
|
965
|
+
if (!streamMarkupHealing) return;
|
|
966
|
+
const calls = streamMarkupHealing.drainCompleted();
|
|
967
|
+
for (const call of calls) emitHealedToolCall(call);
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
// Terminal-chunk bookkeeping for the post-finish grace window below.
|
|
971
|
+
// `streamFinishedAt` flips when a chunk carries `finish_reason`;
|
|
972
|
+
// `sawUsagePayload` flips when a usage payload was parsed.
|
|
973
|
+
let streamFinishedAt: number | undefined;
|
|
974
|
+
let sawUsagePayload = false;
|
|
975
|
+
const timedOpenaiStream = iterateWithIdleTimeout(openaiStream, {
|
|
976
|
+
idleTimeoutMs,
|
|
977
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
978
|
+
firstItemErrorMessage: OPENAI_COMPLETIONS_FIRST_EVENT_TIMEOUT_MESSAGE,
|
|
979
|
+
errorMessage: "OpenAI completions stream stalled while waiting for the next event",
|
|
980
|
+
onIdle: () => requestAbortController.abort(),
|
|
981
|
+
onFirstItemTimeout: () => abortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
982
|
+
abortSignal: options?.signal,
|
|
983
|
+
isProgressItem: isOpenAICompletionsProgressChunk,
|
|
984
|
+
});
|
|
985
|
+
const terminalAwareStream = iterateWithTerminalGrace(timedOpenaiStream, {
|
|
986
|
+
finishedAtMs: () => streamFinishedAt,
|
|
987
|
+
graceMs: OPENAI_COMPLETIONS_POST_FINISH_GRACE_MS,
|
|
988
|
+
// The inner idle-timeout generator is parked mid-`next()` when the
|
|
989
|
+
// grace window closes, so abort the transport to settle that read
|
|
990
|
+
// and release the socket immediately (a queued `.return()` alone
|
|
991
|
+
// would wait on the never-arriving next chunk).
|
|
992
|
+
onGraceEnd: () => requestAbortController.abort(),
|
|
993
|
+
});
|
|
994
|
+
for await (const chunk of terminalAwareStream) {
|
|
995
|
+
if (!chunk || typeof chunk !== "object") continue;
|
|
996
|
+
|
|
997
|
+
// OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
|
|
998
|
+
// and each chunk in a streamed completion carries the same id.
|
|
999
|
+
output.responseId ||= chunk.id;
|
|
1000
|
+
|
|
1001
|
+
// Aggregators (OpenRouter, Vercel AI Gateway, …) report the upstream
|
|
1002
|
+
// provider that actually served the request via a top-level `provider`
|
|
1003
|
+
// field present on every chunk. Capture the first non-empty value so
|
|
1004
|
+
// callers can attribute routing without re-parsing the raw stream.
|
|
1005
|
+
if (!output.upstreamProvider) {
|
|
1006
|
+
const upstreamProvider = (chunk as ProviderAttributedChatCompletionChunk).provider;
|
|
1007
|
+
output.upstreamProvider =
|
|
1008
|
+
typeof upstreamProvider === "string" && upstreamProvider.length > 0 ? upstreamProvider : undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (chunk.usage) {
|
|
1012
|
+
output.usage = parseChunkUsage(chunk.usage, model, premiumRequestsTotal);
|
|
1013
|
+
sawUsagePayload = true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
|
|
1017
|
+
if (!choice) {
|
|
1018
|
+
// Trailing usage-only chunk (`stream_options.include_usage`) after
|
|
1019
|
+
// `finish_reason`: the response is complete — stop pulling instead
|
|
1020
|
+
// of waiting for `[DONE]`/close from hosts that never send either.
|
|
1021
|
+
if (streamFinishedAt !== undefined && sawUsagePayload) break;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (!chunk.usage) {
|
|
1026
|
+
const choiceUsage = (choice as OpenAICompletionsChoiceUsage).usage;
|
|
1027
|
+
if (typeof choiceUsage === "object" && choiceUsage !== null) {
|
|
1028
|
+
output.usage = parseChunkUsage(choiceUsage, model, premiumRequestsTotal);
|
|
1029
|
+
sawUsagePayload = true;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (choice.finish_reason) {
|
|
1034
|
+
const finishReasonResult = mapStopReason(choice.finish_reason);
|
|
1035
|
+
output.stopReason = finishReasonResult.stopReason;
|
|
1036
|
+
if (finishReasonResult.errorMessage) {
|
|
1037
|
+
output.errorMessage = finishReasonResult.errorMessage;
|
|
1038
|
+
}
|
|
1039
|
+
streamFinishedAt ??= Date.now();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (choice.delta) {
|
|
1043
|
+
// Some endpoints return reasoning in reasoning_content (llama.cpp),
|
|
1044
|
+
// or reasoning (other openai compatible endpoints). Use the first
|
|
1045
|
+
// non-empty reasoning field to avoid duplication when a chunk carries
|
|
1046
|
+
// multiple aliases for the same reasoning text.
|
|
1047
|
+
const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
|
|
1048
|
+
const deltaRecord = choice.delta as Record<string, unknown>;
|
|
1049
|
+
let foundReasoningField: string | undefined;
|
|
1050
|
+
let foundReasoningDelta = "";
|
|
1051
|
+
for (const field of reasoningFields) {
|
|
1052
|
+
const reasoningDelta = deltaRecord[field];
|
|
1053
|
+
if (typeof reasoningDelta === "string" && reasoningDelta.length > 0) {
|
|
1054
|
+
foundReasoningField = field;
|
|
1055
|
+
foundReasoningDelta = reasoningDelta;
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (foundReasoningField) {
|
|
1061
|
+
appendThinkingDelta(
|
|
1062
|
+
foundReasoningDelta,
|
|
1063
|
+
foundReasoningField,
|
|
1064
|
+
explicitReasoningDeltasMayBeCumulative ? "cumulative" : "delta",
|
|
1065
|
+
);
|
|
1066
|
+
suppressHealedThinking = true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const normalizedDeltaText = normalizeStreamingContentText(choice.delta.content);
|
|
1070
|
+
if (normalizedDeltaText.length > 0) {
|
|
1071
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1072
|
+
const hasStructuredToolCalls =
|
|
1073
|
+
Array.isArray(choice.delta.tool_calls) && choice.delta.tool_calls.length > 0;
|
|
1074
|
+
|
|
1075
|
+
if (streamMarkupHealing) {
|
|
1076
|
+
const healingEvents = hasStructuredToolCalls
|
|
1077
|
+
? streamMarkupHealing.feedEventsWithoutCalls(normalizedDeltaText)
|
|
1078
|
+
: streamMarkupHealing.feedEvents(normalizedDeltaText);
|
|
1079
|
+
for (const event of healingEvents) {
|
|
1080
|
+
emitHealingEvent(event, suppressHealedThinking);
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
appendProcessedText(normalizedDeltaText);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (choice?.delta?.tool_calls && choice.delta.tool_calls.length > 0) {
|
|
1088
|
+
for (const toolCall of choice.delta.tool_calls) {
|
|
1089
|
+
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
|
1090
|
+
let block = streamIndex !== undefined ? toolCallBlockByIndex.get(streamIndex) : undefined;
|
|
1091
|
+
if (!block && toolCall.id) {
|
|
1092
|
+
block = pendingToolCallBlocks.find(candidate => candidate.id === toolCall.id);
|
|
1093
|
+
}
|
|
1094
|
+
if (
|
|
1095
|
+
!block &&
|
|
1096
|
+
currentBlock?.type === "toolCall" &&
|
|
1097
|
+
(!toolCall.id || currentBlock.id === toolCall.id)
|
|
1098
|
+
) {
|
|
1099
|
+
block = currentBlock;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (!block) {
|
|
1103
|
+
if (currentBlock?.type !== "toolCall") {
|
|
1104
|
+
finishCurrentBlock(currentBlock);
|
|
1105
|
+
}
|
|
1106
|
+
block = {
|
|
1107
|
+
type: "toolCall",
|
|
1108
|
+
id: toolCall.id || "",
|
|
1109
|
+
name: toolCall.function?.name || "",
|
|
1110
|
+
arguments: {},
|
|
1111
|
+
partialArgs: "",
|
|
1112
|
+
streamIndex,
|
|
1113
|
+
};
|
|
1114
|
+
if (streamIndex !== undefined) toolCallBlockByIndex.set(streamIndex, block);
|
|
1115
|
+
pendingToolCallBlocks.push(block);
|
|
1116
|
+
currentBlock = block;
|
|
1117
|
+
output.content.push(block);
|
|
1118
|
+
stream.push({
|
|
1119
|
+
type: "toolcall_start",
|
|
1120
|
+
contentIndex: blockIndex(block),
|
|
1121
|
+
partial: output,
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
// Resuming a pending call after interleaved text/thinking:
|
|
1125
|
+
// close the text/thinking block we drifted into.
|
|
1126
|
+
if (currentBlock !== block && currentBlock && currentBlock.type !== "toolCall") {
|
|
1127
|
+
finishCurrentBlock(currentBlock);
|
|
1128
|
+
}
|
|
1129
|
+
currentBlock = block;
|
|
1130
|
+
if (streamIndex !== undefined && block.streamIndex === undefined) {
|
|
1131
|
+
block.streamIndex = streamIndex;
|
|
1132
|
+
toolCallBlockByIndex.set(streamIndex, block);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (toolCall.id) block.id = toolCall.id;
|
|
1137
|
+
if (toolCall.function?.name) block.name = toolCall.function.name;
|
|
1138
|
+
let delta = "";
|
|
1139
|
+
// The OpenAI SDK types `function.arguments` as a JSON string, but MiniMax-compatible
|
|
1140
|
+
// hosts stream a fully-formed object instead. Model both shapes so the branches below
|
|
1141
|
+
// narrow honestly rather than widening through `unknown`.
|
|
1142
|
+
const rawArgs = toolCall.function?.arguments as string | Record<string, unknown> | undefined;
|
|
1143
|
+
if (typeof rawArgs === "string") {
|
|
1144
|
+
if (rawArgs.length > 0) {
|
|
1145
|
+
delta = rawArgs;
|
|
1146
|
+
const prev = typeof block.partialArgs === "string" ? block.partialArgs : "";
|
|
1147
|
+
block.partialArgs = prev + rawArgs;
|
|
1148
|
+
const throttled = parseStreamingJsonThrottled(
|
|
1149
|
+
block.partialArgs,
|
|
1150
|
+
block[kStreamingLastParseLen] ?? 0,
|
|
1151
|
+
);
|
|
1152
|
+
if (throttled) {
|
|
1153
|
+
block.arguments = throttled.value;
|
|
1154
|
+
block[kStreamingLastParseLen] = throttled.parsedLen;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} else if (rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)) {
|
|
1158
|
+
// MiniMax-compatible hosts stream `function.arguments` as an object instead of the
|
|
1159
|
+
// OpenAI JSON-string contract. Most chunks carry the complete object in one delta,
|
|
1160
|
+
// but cannot rely on that: replacing per-chunk drops earlier keys (and earlier
|
|
1161
|
+
// string content for the same key) when the host fragments the args across deltas.
|
|
1162
|
+
// Deep-merge into the accumulated object. Strings and arrays detect
|
|
1163
|
+
// cumulative-vs-delta semantics by prefix, nested objects merge by key, and
|
|
1164
|
+
// prototype-polluting keys are ignored before storing or comparing values.
|
|
1165
|
+
//
|
|
1166
|
+
// `delta` stays empty here: emitting `JSON.stringify(rawArgs)` per chunk feeds
|
|
1167
|
+
// downstream concat-based accumulators (proxy.ts, openai-chat-server,
|
|
1168
|
+
// openai-responses-server, anthropic-messages-server) an invalid sequence like
|
|
1169
|
+
// `{"input":"a"}{"input":"b"}`. The merged object is flushed as a single
|
|
1170
|
+
// concat-safe delta in `finishToolCallBlock` before `toolcall_end` instead.
|
|
1171
|
+
const prev =
|
|
1172
|
+
block.partialArgs !== null &&
|
|
1173
|
+
typeof block.partialArgs === "object" &&
|
|
1174
|
+
!Array.isArray(block.partialArgs)
|
|
1175
|
+
? (block.partialArgs as Record<string, unknown>)
|
|
1176
|
+
: undefined;
|
|
1177
|
+
const merged = mergeStreamingArgumentObjects(prev, rawArgs);
|
|
1178
|
+
block.partialArgs = merged;
|
|
1179
|
+
block.arguments = merged;
|
|
1180
|
+
}
|
|
1181
|
+
stream.push({
|
|
1182
|
+
type: "toolcall_delta",
|
|
1183
|
+
contentIndex: blockIndex(block),
|
|
1184
|
+
delta,
|
|
1185
|
+
partial: output,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const reasoningDetails = (choice.delta as OpenAICompletionsDeltaWithReasoningDetails).reasoning_details;
|
|
1191
|
+
if (Array.isArray(reasoningDetails)) {
|
|
1192
|
+
for (const detail of reasoningDetails) {
|
|
1193
|
+
if (!detail || typeof detail !== "object") continue;
|
|
1194
|
+
const detailObject = detail as { type?: unknown; id?: unknown; data?: unknown };
|
|
1195
|
+
if (detailObject.type === "reasoning.encrypted" && detailObject.id && detailObject.data) {
|
|
1196
|
+
const matchingToolCall = output.content.find(
|
|
1197
|
+
b => b.type === "toolCall" && b.id === detailObject.id,
|
|
1198
|
+
) as ToolCall | undefined;
|
|
1199
|
+
if (matchingToolCall) {
|
|
1200
|
+
matchingToolCall.thoughtSignature = JSON.stringify(detailObject);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// `finish_reason` + usage both observed: the chat-completions
|
|
1208
|
+
// contract has nothing left to deliver. Break instead of waiting
|
|
1209
|
+
// for `[DONE]`/connection close so hosts that hold the socket open
|
|
1210
|
+
// can't park the turn until the idle watchdog errors it out.
|
|
1211
|
+
if (streamFinishedAt !== undefined && sawUsagePayload) break;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (streamMarkupHealing) {
|
|
1215
|
+
for (const event of streamMarkupHealing.flushEvents()) {
|
|
1216
|
+
emitHealingEvent(event, suppressHealedThinking);
|
|
1217
|
+
}
|
|
1218
|
+
flushHealedToolCalls();
|
|
1219
|
+
if (healedToolCallEmitted && output.stopReason === "stop") {
|
|
1220
|
+
// Hosts that leak tool-call templates often still report
|
|
1221
|
+
// `finish_reason: stop` for the surrounding turn. Promote
|
|
1222
|
+
// only that natural-completion finish — leave `error`,
|
|
1223
|
+
// `length`, `aborted`, etc. untouched.
|
|
1224
|
+
output.stopReason = "toolUse";
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (stripDeepseekChatTemplateTokens) {
|
|
1229
|
+
flushDeepseekStripBuffer(true);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (currentBlock?.type === "toolCall") {
|
|
1233
|
+
finishPendingToolCallBlocks();
|
|
1234
|
+
} else {
|
|
1235
|
+
finishCurrentBlock(currentBlock);
|
|
1236
|
+
finishPendingToolCallBlocks();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Some OpenAI-compatible hosts stream structured `tool_calls` but report
|
|
1240
|
+
// `finish_reason: "stop"` instead of `"tool_calls"`. In the OpenAI contract a
|
|
1241
|
+
// tool call always means "execute and continue", so promote that
|
|
1242
|
+
// natural-completion finish to `toolUse` whenever the turn produced tool-call
|
|
1243
|
+
// blocks — the agent loop gates execution on the stop reason. `error`,
|
|
1244
|
+
// `length`, and `aborted` are intentionally left untouched. (Anthropic's
|
|
1245
|
+
// distinct `end_turn`-with-tool-calls "abandon" semantics live in its own
|
|
1246
|
+
// provider and correctly keep `stop`.)
|
|
1247
|
+
if (output.stopReason === "stop" && output.content.some(b => b.type === "toolCall")) {
|
|
1248
|
+
output.stopReason = "toolUse";
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (
|
|
1252
|
+
policy.stream.emptyLengthFinishIsContextError &&
|
|
1253
|
+
output.stopReason === "length" &&
|
|
1254
|
+
!hasVisibleAssistantContent(output)
|
|
1255
|
+
) {
|
|
1256
|
+
output.stopReason = "error";
|
|
1257
|
+
output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
|
|
1258
|
+
}
|
|
1259
|
+
const localAbortReason = abortTracker.getLocalAbortReason();
|
|
1260
|
+
if (localAbortReason) {
|
|
1261
|
+
throw localAbortReason;
|
|
1262
|
+
}
|
|
1263
|
+
if (abortTracker.wasCallerAbort()) {
|
|
1264
|
+
throw new AIError.AbortError();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (output.stopReason === "aborted") {
|
|
1268
|
+
throw new AIError.AbortError();
|
|
1269
|
+
}
|
|
1270
|
+
if (output.stopReason === "error") {
|
|
1271
|
+
throw new AIError.ProviderResponseError(output.errorMessage || "Provider returned an error stop reason", {
|
|
1272
|
+
provider: model.provider,
|
|
1273
|
+
kind: "runtime",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
output.errorMessage = undefined;
|
|
1278
|
+
output.duration = performance.now() - startTime;
|
|
1279
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1280
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
1281
|
+
stream.end();
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
// Close open blocks first so consumers tracking text_/thinking_/toolcall_
|
|
1284
|
+
// lifecycles never see orphaned starts on the error path. Best-effort: a
|
|
1285
|
+
// throw here must not prevent the terminal error event below.
|
|
1286
|
+
try {
|
|
1287
|
+
finishOpenBlocksOnError();
|
|
1288
|
+
} catch {}
|
|
1289
|
+
const capturedErrorResponse = error instanceof OpenAIHttpError ? error.captured : undefined;
|
|
1290
|
+
const result = await AIError.finalize(error, {
|
|
1291
|
+
api: model.api,
|
|
1292
|
+
provider: model.provider,
|
|
1293
|
+
abortTracker,
|
|
1294
|
+
rawRequestDump,
|
|
1295
|
+
capturedErrorResponse,
|
|
1296
|
+
});
|
|
1297
|
+
output.stopReason = result.stopReason;
|
|
1298
|
+
output.errorStatus = result.status;
|
|
1299
|
+
output.errorId = result.id;
|
|
1300
|
+
output.errorMessage = result.message;
|
|
1301
|
+
// Some providers via OpenRouter include extra details here.
|
|
1302
|
+
const rawMetadata = (error as { error?: { metadata?: { raw?: string } } })?.error?.metadata?.raw;
|
|
1303
|
+
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
1304
|
+
output.duration = performance.now() - startTime;
|
|
1305
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1306
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
1307
|
+
stream.end();
|
|
1308
|
+
}
|
|
1309
|
+
})();
|
|
1310
|
+
|
|
1311
|
+
return stream;
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Public entry: wrap the single-attempt streamer with bounded empty-completion
|
|
1316
|
+
* retries — flaky gateways occasionally 200 with `delta: {}` + `finish_reason:
|
|
1317
|
+
* "stop"` and no usage, which would otherwise stall the agent loop. Shared with
|
|
1318
|
+
* the Anthropic provider via `withEmptyCompletionRetry`.
|
|
1319
|
+
*/
|
|
1320
|
+
export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (model, context, options) =>
|
|
1321
|
+
withEmptyCompletionRetry(model, context, options, streamOpenAICompletionsOnce);
|
|
1322
|
+
|
|
1323
|
+
function createRequestSetup(
|
|
1324
|
+
model: Model<"openai-completions">,
|
|
1325
|
+
context: Context,
|
|
1326
|
+
apiKey?: string,
|
|
1327
|
+
extraHeaders?: Record<string, string>,
|
|
1328
|
+
initiatorOverride?: MessageAttribution,
|
|
1329
|
+
): OpenAIRequestSetup & { baseUrl: string } {
|
|
1330
|
+
const apiVersion = $env.AZURE_OPENAI_API_VERSION || "2024-10-21";
|
|
1331
|
+
const deploymentName = parseAzureDeploymentNameMap($env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).get(model.id) ?? model.id;
|
|
1332
|
+
const setup = resolveOpenAIRequestSetup(model, {
|
|
1333
|
+
apiKey,
|
|
1334
|
+
extraHeaders,
|
|
1335
|
+
initiatorOverride,
|
|
1336
|
+
messages: context.messages,
|
|
1337
|
+
defaultBaseUrl: "https://api.openai.com/v1",
|
|
1338
|
+
// Provider auth/header overlay: Kimi-code hosts require shared client
|
|
1339
|
+
// attribution headers prepended before caller headers. Kept here (not in
|
|
1340
|
+
// the shared helper) because it is provider-specific request setup.
|
|
1341
|
+
prependHeaders: model.provider === "kimi-code" ? getKimiCommonHeaders : undefined,
|
|
1342
|
+
alibabaCodingPlanAuth: true,
|
|
1343
|
+
azureChatCompletions: { apiVersion, deploymentName },
|
|
1344
|
+
});
|
|
1345
|
+
if (!setup.baseUrl) {
|
|
1346
|
+
throw new AIError.ConfigurationError("OpenAI request setup did not resolve a base URL");
|
|
1347
|
+
}
|
|
1348
|
+
return setup as OpenAIRequestSetup & { baseUrl: string };
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function resolveOpenAICompatForRequest(
|
|
1352
|
+
model: Model<"openai-completions">,
|
|
1353
|
+
options: OpenAICompletionsOptions | undefined,
|
|
1354
|
+
): OpenAICompatPolicy {
|
|
1355
|
+
return resolveOpenAICompatPolicy(model, {
|
|
1356
|
+
endpoint: "chat-completions",
|
|
1357
|
+
reasoning: options?.reasoning,
|
|
1358
|
+
disableReasoning: options?.disableReasoning,
|
|
1359
|
+
toolChoice: mapToOpenAICompletionsToolChoice(options?.toolChoice),
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function dropOpenRouterKimiForcedToolReasoning(
|
|
1364
|
+
params: OpenAICompletionsParams,
|
|
1365
|
+
model: Model<"openai-completions">,
|
|
1366
|
+
policy: OpenAICompatPolicy,
|
|
1367
|
+
): void {
|
|
1368
|
+
if (
|
|
1369
|
+
policy.reasoning.disableReason === "forced-tool-choice" &&
|
|
1370
|
+
policy.reasoning.disableMode === "openrouter-enabled-false" &&
|
|
1371
|
+
policy.compat.isOpenRouterHost &&
|
|
1372
|
+
isKimiModelId(model.id)
|
|
1373
|
+
) {
|
|
1374
|
+
delete params.reasoning;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function buildParams(
|
|
1379
|
+
model: Model<"openai-completions">,
|
|
1380
|
+
context: Context,
|
|
1381
|
+
options: OpenAICompletionsOptions | undefined,
|
|
1382
|
+
toolStrictModeOverride?: ToolStrictModeOverride,
|
|
1383
|
+
): { params: OpenAICompletionsParams; toolStrictMode: AppliedToolStrictMode; strictToolsApplied: boolean } {
|
|
1384
|
+
const initialPolicy = resolveOpenAICompatForRequest(model, options);
|
|
1385
|
+
const initialCompat = initialPolicy.compat as ResolvedOpenAICompat;
|
|
1386
|
+
|
|
1387
|
+
const requestModelId = resolveOpenAICompletionsModelId(model, options);
|
|
1388
|
+
const params: OpenAICompletionsParams = {
|
|
1389
|
+
model: requestModelId,
|
|
1390
|
+
messages: [],
|
|
1391
|
+
stream: true,
|
|
1392
|
+
};
|
|
1393
|
+
let toolStrictMode: AppliedToolStrictMode = "none";
|
|
1394
|
+
let strictToolsApplied = false;
|
|
1395
|
+
|
|
1396
|
+
if (initialCompat.supportsUsageInStreaming !== false) {
|
|
1397
|
+
params.stream_options = { include_usage: true };
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (initialCompat.supportsStore) {
|
|
1401
|
+
params.store = false;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (options?.temperature !== undefined) {
|
|
1405
|
+
params.temperature = options.temperature;
|
|
1406
|
+
}
|
|
1407
|
+
if (options?.topP !== undefined) {
|
|
1408
|
+
params.top_p = options.topP;
|
|
1409
|
+
}
|
|
1410
|
+
if (options?.topK !== undefined) {
|
|
1411
|
+
params.top_k = options.topK;
|
|
1412
|
+
}
|
|
1413
|
+
if (options?.minP !== undefined) {
|
|
1414
|
+
params.min_p = options.minP;
|
|
1415
|
+
}
|
|
1416
|
+
if (options?.presencePenalty !== undefined) {
|
|
1417
|
+
params.presence_penalty = options.presencePenalty;
|
|
1418
|
+
}
|
|
1419
|
+
if (options?.repetitionPenalty !== undefined) {
|
|
1420
|
+
params.repetition_penalty = options.repetitionPenalty;
|
|
1421
|
+
}
|
|
1422
|
+
if (options?.stopSequences?.length) {
|
|
1423
|
+
const seqs = options.stopSequences;
|
|
1424
|
+
params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
|
|
1425
|
+
}
|
|
1426
|
+
if (options?.frequencyPenalty !== undefined) {
|
|
1427
|
+
params.frequency_penalty = options.frequencyPenalty;
|
|
1428
|
+
}
|
|
1429
|
+
applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
|
|
1430
|
+
|
|
1431
|
+
if (context.tools?.length) {
|
|
1432
|
+
const builtTools = convertTools(context.tools, initialCompat, toolStrictModeOverride);
|
|
1433
|
+
params.tools = builtTools.tools;
|
|
1434
|
+
toolStrictMode = builtTools.toolStrictMode;
|
|
1435
|
+
strictToolsApplied = builtTools.strictToolsApplied;
|
|
1436
|
+
} else if (context.tools === undefined && hasToolHistory(context.messages)) {
|
|
1437
|
+
// Anthropic (via LiteLLM/proxy) requires the `tools` param when the conversation
|
|
1438
|
+
// contains tool_calls/tool_results, even when no tools are offered this turn.
|
|
1439
|
+
// Only inject the sentinel when the caller passed `context.tools = undefined`
|
|
1440
|
+
// (i.e. tools were not specified at all). An explicit `context.tools = []` means
|
|
1441
|
+
// the caller opted out of tools for this turn (as /btw and IRC background replies
|
|
1442
|
+
// do via AgentSession.runEphemeralTurn) — honour that intent and emit nothing,
|
|
1443
|
+
// so LiteLLM → Bedrock never sees an empty `toolConfig` block.
|
|
1444
|
+
params.tools = [];
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (options?.toolChoice && initialCompat.supportsToolChoice) {
|
|
1448
|
+
params.tool_choice = mapToOpenAICompletionsToolChoice(options.toolChoice);
|
|
1449
|
+
}
|
|
1450
|
+
if (
|
|
1451
|
+
typeof params.tool_choice === "object" &&
|
|
1452
|
+
params.tool_choice !== null &&
|
|
1453
|
+
!initialCompat.supportsNamedToolChoice
|
|
1454
|
+
) {
|
|
1455
|
+
params.tool_choice = "required";
|
|
1456
|
+
}
|
|
1457
|
+
if (isForcedToolChoice(params.tool_choice) && !initialCompat.supportsForcedToolChoice) {
|
|
1458
|
+
// Some thinking-required OpenAI-compatible models reject forced
|
|
1459
|
+
// `tool_choice` while still accepting tools with the default auto
|
|
1460
|
+
// selector. Keep the tool available and let the model choose it.
|
|
1461
|
+
params.tool_choice = "auto";
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (params.tool_choice === "none" && (!Array.isArray(params.tools) || params.tools.length === 0)) {
|
|
1465
|
+
// `tool_choice: "none"` with no tools to gate is redundant and also
|
|
1466
|
+
// trips LiteLLM → Bedrock: the proxy serializes the directive into a
|
|
1467
|
+
// `toolConfig` block, and Bedrock requires `toolConfig.tools` to be
|
|
1468
|
+
// non-empty whenever the conversation already holds `toolUse`/`toolResult`
|
|
1469
|
+
// content. Drop it whenever the resolved tools list is missing or empty.
|
|
1470
|
+
// Side-channel turns hit this: `/btw` and IRC background replies route
|
|
1471
|
+
// through `AgentSession.runEphemeralTurn`, which sets `context.tools = []`
|
|
1472
|
+
// and `toolChoice: "none"` (see packages/coding-agent/src/session/agent-session.ts).
|
|
1473
|
+
delete params.tool_choice;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const forcedToolName =
|
|
1477
|
+
typeof params.tool_choice === "object" && params.tool_choice !== null && "function" in params.tool_choice
|
|
1478
|
+
? params.tool_choice.function.name
|
|
1479
|
+
: undefined;
|
|
1480
|
+
if (
|
|
1481
|
+
forcedToolName !== undefined &&
|
|
1482
|
+
(!Array.isArray(params.tools) ||
|
|
1483
|
+
!params.tools.some(tool => tool.type === "function" && tool.function.name === forcedToolName))
|
|
1484
|
+
) {
|
|
1485
|
+
// A forced named tool_choice is only valid when the same request offers
|
|
1486
|
+
// that function in `tools`. Active-tool filtering normally enforces this
|
|
1487
|
+
// before provider dispatch; this guard keeps raw provider callers from
|
|
1488
|
+
// emitting a self-inconsistent OpenAI-compatible payload.
|
|
1489
|
+
delete params.tool_choice;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const finalPolicy = resolveOpenAICompatPolicy(model, {
|
|
1493
|
+
endpoint: "chat-completions",
|
|
1494
|
+
reasoning: options?.reasoning,
|
|
1495
|
+
disableReasoning: options?.disableReasoning,
|
|
1496
|
+
toolChoice: params.tool_choice,
|
|
1497
|
+
});
|
|
1498
|
+
const compat = finalPolicy.compat as ResolvedOpenAICompat;
|
|
1499
|
+
const messages = convertMessages(model, context, compat);
|
|
1500
|
+
maybeAddAnthropicCacheControl(compat, messages);
|
|
1501
|
+
params.messages = messages;
|
|
1502
|
+
const outputToken = resolveOpenAIOutputTokenParam({
|
|
1503
|
+
field: compat.maxTokensField,
|
|
1504
|
+
maxTokens: options?.maxTokens,
|
|
1505
|
+
maxTokensExplicit: options?.maxTokensExplicit ?? options?.maxTokens !== undefined,
|
|
1506
|
+
modelMaxTokens: model.maxTokens,
|
|
1507
|
+
omitMaxOutputTokens: model.omitMaxOutputTokens ?? false,
|
|
1508
|
+
isOpenRouterHost: compat.isOpenRouterHost,
|
|
1509
|
+
alwaysSendMaxTokens: compat.alwaysSendMaxTokens,
|
|
1510
|
+
providerOutputClamp: resolveZaiReasoningOutputClamp(model, compat),
|
|
1511
|
+
});
|
|
1512
|
+
if (outputToken) {
|
|
1513
|
+
if (outputToken.field === "max_tokens") {
|
|
1514
|
+
params.max_tokens = outputToken.value;
|
|
1515
|
+
} else if (outputToken.field === "max_completion_tokens") {
|
|
1516
|
+
params.max_completion_tokens = outputToken.value;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
applyChatCompletionsToolStream(params, model, compat);
|
|
1520
|
+
|
|
1521
|
+
applyChatCompletionsCompatPolicy(params, finalPolicy);
|
|
1522
|
+
dropOpenRouterKimiForcedToolReasoning(params, model, finalPolicy);
|
|
1523
|
+
|
|
1524
|
+
applyOpenAIGatewayRouting(params, compat);
|
|
1525
|
+
|
|
1526
|
+
applyOpenAIExtraBody(params, compat.extraBody, {
|
|
1527
|
+
dropThinkingWhenReasoningEffort: compat.dropThinkingWhenReasoningEffort,
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
return { params, toolStrictMode, strictToolsApplied };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
export function parseChunkUsage(
|
|
1534
|
+
rawUsage: object,
|
|
1535
|
+
model: Model<"openai-completions">,
|
|
1536
|
+
premiumRequests: number | undefined,
|
|
1537
|
+
): AssistantMessage["usage"] {
|
|
1538
|
+
const usageLike = rawUsage as OpenAICompletionsUsageLike;
|
|
1539
|
+
const rawPromptTokenDetails = usageLike.prompt_tokens_details;
|
|
1540
|
+
const promptTokenDetails =
|
|
1541
|
+
typeof rawPromptTokenDetails === "object" && rawPromptTokenDetails !== null
|
|
1542
|
+
? (rawPromptTokenDetails as OpenAICompletionsPromptTokenDetails)
|
|
1543
|
+
: undefined;
|
|
1544
|
+
const rawCompletionTokenDetails = usageLike.completion_tokens_details;
|
|
1545
|
+
const completionTokenDetails =
|
|
1546
|
+
typeof rawCompletionTokenDetails === "object" && rawCompletionTokenDetails !== null
|
|
1547
|
+
? (rawCompletionTokenDetails as OpenAICompletionsCompletionTokenDetails)
|
|
1548
|
+
: undefined;
|
|
1549
|
+
const completionTokens = usageLike.completion_tokens;
|
|
1550
|
+
const promptTokens = usageLike.prompt_tokens;
|
|
1551
|
+
const cachedTokens = usageLike.cached_tokens;
|
|
1552
|
+
const promptCacheHitTokens = usageLike.prompt_cache_hit_tokens;
|
|
1553
|
+
const promptCacheMissTokens = usageLike.prompt_cache_miss_tokens;
|
|
1554
|
+
const promptTokenCachedTokens = promptTokenDetails?.cached_tokens;
|
|
1555
|
+
const completionReasoningTokens = completionTokenDetails?.reasoning_tokens;
|
|
1556
|
+
const cacheWriteTokens = promptTokenDetails?.cache_write_tokens;
|
|
1557
|
+
const outputTokens = typeof completionTokens === "number" ? completionTokens : 0;
|
|
1558
|
+
const accounting = calculateOpenAIUsageAccounting({
|
|
1559
|
+
promptTokens: typeof promptTokens === "number" ? promptTokens : 0,
|
|
1560
|
+
outputTokens,
|
|
1561
|
+
cachedTokens:
|
|
1562
|
+
(typeof cachedTokens === "number" ? cachedTokens : undefined) ??
|
|
1563
|
+
(typeof promptCacheHitTokens === "number" ? promptCacheHitTokens : undefined) ??
|
|
1564
|
+
(typeof promptTokenCachedTokens === "number" ? promptTokenCachedTokens : undefined) ??
|
|
1565
|
+
0,
|
|
1566
|
+
reasoningTokens: typeof completionReasoningTokens === "number" ? completionReasoningTokens : 0,
|
|
1567
|
+
cacheWriteOpenRouter: typeof cacheWriteTokens === "number" ? cacheWriteTokens : undefined,
|
|
1568
|
+
cacheWriteDeepSeek: typeof promptCacheMissTokens === "number" ? promptCacheMissTokens : undefined,
|
|
1569
|
+
hasDeepSeekCacheHitAndMiss: typeof promptCacheHitTokens === "number" && typeof promptCacheMissTokens === "number",
|
|
1570
|
+
});
|
|
1571
|
+
const usage: AssistantMessage["usage"] = {
|
|
1572
|
+
...accounting,
|
|
1573
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1574
|
+
...(premiumRequests !== undefined ? { premiumRequests } : {}),
|
|
1575
|
+
};
|
|
1576
|
+
calculateCost(model, usage);
|
|
1577
|
+
return usage;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function maybeAddAnthropicCacheControl(compat: ResolvedOpenAICompat, messages: ChatCompletionMessageParam[]): void {
|
|
1581
|
+
if (compat.cacheControlFormat !== "anthropic") return;
|
|
1582
|
+
// Anthropic-style caching requires cache_control on a text part. Add a breakpoint
|
|
1583
|
+
// on the last user/assistant message (walking backwards until we find text content).
|
|
1584
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1585
|
+
const msg = messages[i];
|
|
1586
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "developer") continue;
|
|
1587
|
+
|
|
1588
|
+
const content = msg.content;
|
|
1589
|
+
if (typeof content === "string") {
|
|
1590
|
+
if (content.trim().length === 0) continue;
|
|
1591
|
+
msg.content = [
|
|
1592
|
+
Object.assign({ type: "text" as const, text: content }, { cache_control: { type: "ephemeral" } }),
|
|
1593
|
+
];
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (!Array.isArray(content)) continue;
|
|
1598
|
+
|
|
1599
|
+
// Find last non-empty text part and add cache_control. Empty assistant
|
|
1600
|
+
// content is valid for tool-call replay, but Anthropic/OpenRouter reject
|
|
1601
|
+
// empty text blocks once cache_control turns it into structured content.
|
|
1602
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
1603
|
+
const part = content[j];
|
|
1604
|
+
if (part?.type === "text" && part.text.trim().length > 0) {
|
|
1605
|
+
Object.assign(part, { cache_control: { type: "ephemeral" } });
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
export function convertMessages(
|
|
1613
|
+
model: Model<"openai-completions">,
|
|
1614
|
+
context: Context,
|
|
1615
|
+
compat: ResolvedOpenAICompat,
|
|
1616
|
+
): ChatCompletionMessageParam[] {
|
|
1617
|
+
const params: ChatCompletionMessageParam[] = [];
|
|
1618
|
+
|
|
1619
|
+
const maxNormalizedToolCallIdLength = compat.requiresMistralToolIds
|
|
1620
|
+
? 9
|
|
1621
|
+
: compat.usesOpenAIToolCallIdLimit
|
|
1622
|
+
? 40
|
|
1623
|
+
: undefined;
|
|
1624
|
+
const duplicateToolCallIdSuffixPrefix = compat.requiresMistralToolIds ? "dup" : undefined;
|
|
1625
|
+
const normalizeToolCallId = (id: string): string => {
|
|
1626
|
+
if (compat.requiresMistralToolIds) return normalizeMistralToolId(id, true);
|
|
1627
|
+
|
|
1628
|
+
// Handle pipe-separated IDs from OpenAI Responses API
|
|
1629
|
+
// Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
|
|
1630
|
+
// These come from providers like github-copilot, openai-codex, opencode
|
|
1631
|
+
// Extract just the call_id part and normalize it
|
|
1632
|
+
if (id.includes("|")) {
|
|
1633
|
+
const [callId] = id.split("|");
|
|
1634
|
+
// Sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
|
|
1635
|
+
return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (compat.usesOpenAIToolCallIdLimit) return id.length > 40 ? id.slice(0, 40) : id;
|
|
1639
|
+
return id;
|
|
1640
|
+
};
|
|
1641
|
+
const transformedMessages = transformMessages(
|
|
1642
|
+
context.messages,
|
|
1643
|
+
model,
|
|
1644
|
+
id => normalizeToolCallId(id),
|
|
1645
|
+
maxNormalizedToolCallIdLength,
|
|
1646
|
+
duplicateToolCallIdSuffixPrefix,
|
|
1647
|
+
compat,
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
const remappedToolCallIds = new Map<string, string[]>();
|
|
1651
|
+
let generatedToolCallIdCounter = 0;
|
|
1652
|
+
|
|
1653
|
+
const generateFallbackToolCallId = (seed: string): string => {
|
|
1654
|
+
generatedToolCallIdCounter += 1;
|
|
1655
|
+
const hash = Bun.hash(`${model.provider}:${model.id}:${seed}:${generatedToolCallIdCounter}`).toString(36);
|
|
1656
|
+
return `call_${hash}`;
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const rememberToolCallId = (originalId: string, normalizedId: string): void => {
|
|
1660
|
+
const queue = remappedToolCallIds.get(originalId);
|
|
1661
|
+
if (queue) {
|
|
1662
|
+
queue.push(normalizedId);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
remappedToolCallIds.set(originalId, [normalizedId]);
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
const consumeToolCallId = (originalId: string): string | null => {
|
|
1669
|
+
const queue = remappedToolCallIds.get(originalId);
|
|
1670
|
+
if (!queue || queue.length === 0) return null;
|
|
1671
|
+
const nextId = queue.shift() ?? null;
|
|
1672
|
+
if (queue.length === 0) remappedToolCallIds.delete(originalId);
|
|
1673
|
+
return nextId;
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const ensureToolCallId = (rawId: string, seed: string): string => {
|
|
1677
|
+
const normalized = normalizeToolCallId(rawId);
|
|
1678
|
+
if (normalized.trim().length > 0) return normalized;
|
|
1679
|
+
return generateFallbackToolCallId(seed);
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
1683
|
+
if (systemPrompts.length > 0) {
|
|
1684
|
+
const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
|
|
1685
|
+
const role = useDeveloperRole ? "developer" : "system";
|
|
1686
|
+
// Default to one block per ordered system prompt so the leading prefix
|
|
1687
|
+
// stays byte-identical between turns and the provider's KV cache can
|
|
1688
|
+
// reuse it. Hosts whose chat templates reject follow-up system messages
|
|
1689
|
+
// (Qwen via vLLM, MiniMax, Alibaba Dashscope, Qwen Portal, …) opt out
|
|
1690
|
+
// via `compat.supportsMultipleSystemMessages = false`; in that mode we
|
|
1691
|
+
// coalesce into a single message joined by `\n\n`.
|
|
1692
|
+
if (compat.supportsMultipleSystemMessages) {
|
|
1693
|
+
for (const systemPrompt of systemPrompts) {
|
|
1694
|
+
params.push({ role, content: systemPrompt });
|
|
1695
|
+
}
|
|
1696
|
+
} else {
|
|
1697
|
+
params.push({ role, content: systemPrompts.join("\n\n") });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
let lastRole: string | null = null;
|
|
1702
|
+
|
|
1703
|
+
for (let i = 0; i < transformedMessages.length; i++) {
|
|
1704
|
+
const msg = transformedMessages[i];
|
|
1705
|
+
// Some providers (e.g. Mistral/Devstral) don't allow user messages directly after tool results
|
|
1706
|
+
// Insert a synthetic assistant message to bridge the gap
|
|
1707
|
+
if (
|
|
1708
|
+
compat.requiresAssistantAfterToolResult &&
|
|
1709
|
+
lastRole === "toolResult" &&
|
|
1710
|
+
(msg.role === "user" || msg.role === "developer")
|
|
1711
|
+
) {
|
|
1712
|
+
params.push({
|
|
1713
|
+
role: "assistant",
|
|
1714
|
+
content: "I have processed the tool results.",
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const devAsUser = !compat.supportsDeveloperRole;
|
|
1719
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
1720
|
+
const role = !devAsUser && msg.role === "developer" ? "developer" : "user";
|
|
1721
|
+
if (typeof msg.content === "string") {
|
|
1722
|
+
const text = msg.content.toWellFormed();
|
|
1723
|
+
if (text.trim().length === 0) continue;
|
|
1724
|
+
params.push({
|
|
1725
|
+
role: role,
|
|
1726
|
+
content: text,
|
|
1727
|
+
});
|
|
1728
|
+
} else {
|
|
1729
|
+
const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
|
|
1730
|
+
const content: ChatCompletionContentPart[] = [];
|
|
1731
|
+
let omittedImages = false;
|
|
1732
|
+
for (const item of msg.content) {
|
|
1733
|
+
if (item.type === "text") {
|
|
1734
|
+
const text = item.text.toWellFormed();
|
|
1735
|
+
if (text.trim().length === 0) continue;
|
|
1736
|
+
content.push({
|
|
1737
|
+
type: "text",
|
|
1738
|
+
text,
|
|
1739
|
+
} satisfies ChatCompletionContentPartText);
|
|
1740
|
+
} else if (supportsImages) {
|
|
1741
|
+
content.push({
|
|
1742
|
+
type: "image_url",
|
|
1743
|
+
image_url: {
|
|
1744
|
+
url: `data:${item.mimeType};base64,${item.data}`,
|
|
1745
|
+
// Chat Completions has no "original"; omit it (provider default).
|
|
1746
|
+
...(item.detail && item.detail !== "original" ? { detail: item.detail } : {}),
|
|
1747
|
+
},
|
|
1748
|
+
} satisfies ChatCompletionContentPartImage);
|
|
1749
|
+
} else {
|
|
1750
|
+
omittedImages = true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (omittedImages) {
|
|
1754
|
+
content.push({
|
|
1755
|
+
type: "text",
|
|
1756
|
+
text: NON_VISION_IMAGE_PLACEHOLDER,
|
|
1757
|
+
} satisfies ChatCompletionContentPartText);
|
|
1758
|
+
}
|
|
1759
|
+
if (content.length === 0) continue;
|
|
1760
|
+
params.push({
|
|
1761
|
+
role: "user",
|
|
1762
|
+
content,
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
} else if (msg.role === "assistant") {
|
|
1766
|
+
const assistantMsg: OpenAICompletionsAssistantMessageParam = {
|
|
1767
|
+
role: "assistant",
|
|
1768
|
+
content: null,
|
|
1769
|
+
};
|
|
1770
|
+
|
|
1771
|
+
const textBlocks = msg.content.filter(b => b.type === "text") as TextContent[];
|
|
1772
|
+
// Filter out empty text blocks to avoid API validation errors
|
|
1773
|
+
const nonEmptyTextBlocks = textBlocks.filter(b => b.text && b.text.trim().length > 0);
|
|
1774
|
+
if (nonEmptyTextBlocks.length > 0) {
|
|
1775
|
+
// Always send assistant content as a plain string. Some OpenAI-compatible
|
|
1776
|
+
// backends mirror array-of-text-block payloads back to the model literally,
|
|
1777
|
+
// causing recursive nested content in subsequent turns.
|
|
1778
|
+
assistantMsg.content = nonEmptyTextBlocks.map(b => b.text.toWellFormed()).join("");
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Handle thinking blocks
|
|
1782
|
+
const thinkingBlocks = msg.content.filter(b => b.type === "thinking") as ThinkingContent[];
|
|
1783
|
+
// Filter out empty thinking blocks to avoid API validation errors
|
|
1784
|
+
const nonEmptyThinkingBlocks = thinkingBlocks.filter(b => b.thinking && b.thinking.trim().length > 0);
|
|
1785
|
+
if (nonEmptyThinkingBlocks.length > 0) {
|
|
1786
|
+
if (compat.requiresThinkingAsText) {
|
|
1787
|
+
const thinkingText = nonEmptyThinkingBlocks
|
|
1788
|
+
.map(b => renderDemotedThinking(model.id, b.thinking))
|
|
1789
|
+
.join("");
|
|
1790
|
+
// `content` is a plain string at this point (set above) or null —
|
|
1791
|
+
// never an array. Prepend the demoted thinking to the string form.
|
|
1792
|
+
assistantMsg.content =
|
|
1793
|
+
typeof assistantMsg.content === "string" && assistantMsg.content.length > 0
|
|
1794
|
+
? `${thinkingText}${assistantMsg.content}`
|
|
1795
|
+
: thinkingText;
|
|
1796
|
+
} else if (compat.requiresReasoningContentForToolCalls) {
|
|
1797
|
+
// Use the streamed signature when the backend accepts whichever
|
|
1798
|
+
// recognized field name was emitted (allowsSynthetic=true). Backends
|
|
1799
|
+
// like opencode-kimi-with-thinking and DeepSeek demand the exact
|
|
1800
|
+
// configured `reasoningContentField` instead, so honor that here
|
|
1801
|
+
// rather than echoing the upstream field name.
|
|
1802
|
+
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
|
1803
|
+
const wireField =
|
|
1804
|
+
compat.allowsSyntheticReasoningContentForToolCalls &&
|
|
1805
|
+
(signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text")
|
|
1806
|
+
? signature
|
|
1807
|
+
: signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text"
|
|
1808
|
+
? (compat.reasoningContentField ?? "reasoning_content")
|
|
1809
|
+
: undefined;
|
|
1810
|
+
if (wireField) {
|
|
1811
|
+
assistantMsg[wireField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
|
|
1812
|
+
}
|
|
1813
|
+
} else if (compat.thinkingFormat === "zai" && model.reasoning) {
|
|
1814
|
+
// Z.AI / Zhipu / Moonshot Kimi (native) / Xiaomi MiMo accept
|
|
1815
|
+
// `reasoning_content` as a continuation hint even when they don't
|
|
1816
|
+
// strictly require it. Surfacing the preserved thinking text here
|
|
1817
|
+
// keeps cross-API replays (Z.AI Anthropic → Z.AI OpenAI, etc.)
|
|
1818
|
+
// shipping reasoning as structured `reasoning_content` rather than
|
|
1819
|
+
// folded into conversation text (#3434). Signature is irrelevant on
|
|
1820
|
+
// this path: `transform-messages` strips the source wire-format
|
|
1821
|
+
// signature on cross-API replays before the block reaches us.
|
|
1822
|
+
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
|
|
1823
|
+
assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
|
|
1824
|
+
} else if (compat.replayReasoningContent) {
|
|
1825
|
+
// Local llama.cpp-style servers (llama.cpp, LM Studio, vLLM, Ollama
|
|
1826
|
+
// in openai-completions mode, custom providers pointed at a
|
|
1827
|
+
// loopback baseUrl) re-tokenize the entire prompt every request.
|
|
1828
|
+
// Qwen3 / DeepSeek-R1 / GLM chat templates reconstruct the prior
|
|
1829
|
+
// assistant turn's `<think>` block from `reasoning_content`; if we
|
|
1830
|
+
// drop the field the template re-renders the assistant turn
|
|
1831
|
+
// without thinking content, the rendered tokens diverge from the
|
|
1832
|
+
// slot's existing KV cache, and llama.cpp falls back to full
|
|
1833
|
+
// prompt re-processing (#3528). Honor the streamed signature when
|
|
1834
|
+
// it identifies a recognized wire field so a model that emitted
|
|
1835
|
+
// `reasoning` (some llama.cpp builds) round-trips to the same
|
|
1836
|
+
// field; otherwise fall back to the configured
|
|
1837
|
+
// `reasoningContentField`. Gated by the new compat flag rather
|
|
1838
|
+
// than the existing `requires*` flags because local servers
|
|
1839
|
+
// accept but don't validate the field — they just need it to
|
|
1840
|
+
// preserve cache locality.
|
|
1841
|
+
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
|
|
1842
|
+
const reasoningField: OpenAICompletionsReasoningField =
|
|
1843
|
+
signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text"
|
|
1844
|
+
? signature
|
|
1845
|
+
: (compat.reasoningContentField ?? "reasoning_content");
|
|
1846
|
+
assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
if (compat.requiresReasoningContentForToolCalls) {
|
|
1851
|
+
const streamedReasoningField = nonEmptyThinkingBlocks[0]?.thinkingSignature;
|
|
1852
|
+
const reasoningField =
|
|
1853
|
+
compat.allowsSyntheticReasoningContentForToolCalls &&
|
|
1854
|
+
(streamedReasoningField === "reasoning_content" ||
|
|
1855
|
+
streamedReasoningField === "reasoning" ||
|
|
1856
|
+
streamedReasoningField === "reasoning_text")
|
|
1857
|
+
? streamedReasoningField
|
|
1858
|
+
: (compat.reasoningContentField ?? "reasoning_content");
|
|
1859
|
+
const reasoningContent = assistantMsg[reasoningField];
|
|
1860
|
+
if (!reasoningContent) {
|
|
1861
|
+
const reasoning = assistantMsg.reasoning;
|
|
1862
|
+
const reasoningText = assistantMsg.reasoning_text;
|
|
1863
|
+
if (reasoning && reasoningField !== "reasoning") {
|
|
1864
|
+
assistantMsg[reasoningField] = reasoning;
|
|
1865
|
+
} else if (reasoningText && reasoningField !== "reasoning_text") {
|
|
1866
|
+
assistantMsg[reasoningField] = reasoningText;
|
|
1867
|
+
} else if (nonEmptyThinkingBlocks.length > 0) {
|
|
1868
|
+
assistantMsg[reasoningField] = nonEmptyThinkingBlocks.map(b => b.thinking).join("\n");
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const toolCalls = msg.content.filter(b => b.type === "toolCall") as ToolCall[];
|
|
1874
|
+
// Replay reasoning_content on assistant turns for backends that validate
|
|
1875
|
+
// thinking-mode history. DeepSeek V4 requires reasoning_content on EVERY
|
|
1876
|
+
// assistant turn once a prior turn included it — not just tool-call turns.
|
|
1877
|
+
// The replay logic has three tiers:
|
|
1878
|
+
// 1. Recover from thinking blocks with valid signatures (covers same-model replay
|
|
1879
|
+
// where nonEmptyThinkingBlocks may have filtered out empty-text blocks)
|
|
1880
|
+
// 2. For providers that require the field but returned no reasoning at all
|
|
1881
|
+
// (e.g. proxy-stripped reasoning_content), emit an empty string
|
|
1882
|
+
// 3. For providers that accept synthetic placeholders (Kimi, OpenRouter), emit "."
|
|
1883
|
+
// DeepSeek V4 rejects synthetic "." placeholders — it validates the exact value —
|
|
1884
|
+
// so the allowsSyntheticReasoningContentForToolCalls flag controls tier 3.
|
|
1885
|
+
const canUseSyntheticReasoningContent =
|
|
1886
|
+
compat.requiresReasoningContentForToolCalls &&
|
|
1887
|
+
compat.allowsSyntheticReasoningContentForToolCalls &&
|
|
1888
|
+
(compat.thinkingFormat === "openai" ||
|
|
1889
|
+
compat.thinkingFormat === "openrouter" ||
|
|
1890
|
+
compat.thinkingFormat === "zai");
|
|
1891
|
+
// DeepSeek-compatible reasoning models require reasoning_content on all
|
|
1892
|
+
// assistant turns. Providers that allow placeholders only need it on
|
|
1893
|
+
// tool-call turns.
|
|
1894
|
+
const needsReasoningOnAllTurns = compat.requiresReasoningContentForAllAssistantTurns;
|
|
1895
|
+
const needsReasoningField = needsReasoningOnAllTurns || toolCalls.length > 0;
|
|
1896
|
+
let hasReasoningField =
|
|
1897
|
+
assistantMsg.reasoning_content !== undefined ||
|
|
1898
|
+
assistantMsg.reasoning !== undefined ||
|
|
1899
|
+
assistantMsg.reasoning_text !== undefined;
|
|
1900
|
+
// Tier 1: Recover reasoning_content from ALL thinking blocks (including empty-text
|
|
1901
|
+
// ones) when the provider requires exact replay and rejects synthetic placeholders.
|
|
1902
|
+
// This covers the case where thinking blocks have valid signatures but were excluded
|
|
1903
|
+
// by the nonEmptyThinkingBlocks filter above, or where thinking text is empty but
|
|
1904
|
+
// the signature identifies the correct field name for replay.
|
|
1905
|
+
// Only recognized OpenAI-compat reasoning field names qualify — opaque signatures
|
|
1906
|
+
// from other providers (Anthropic encrypted, OpenAI Responses JSON, etc.) are not
|
|
1907
|
+
// valid property names for the wire message.
|
|
1908
|
+
if (
|
|
1909
|
+
needsReasoningField &&
|
|
1910
|
+
!hasReasoningField &&
|
|
1911
|
+
compat.requiresReasoningContentForToolCalls &&
|
|
1912
|
+
!compat.allowsSyntheticReasoningContentForToolCalls
|
|
1913
|
+
) {
|
|
1914
|
+
const allThinkingBlocks = msg.content.filter(b => b.type === "thinking") as ThinkingContent[];
|
|
1915
|
+
if (allThinkingBlocks.length > 0) {
|
|
1916
|
+
const signature = allThinkingBlocks[0].thinkingSignature;
|
|
1917
|
+
if (signature === "reasoning_content" || signature === "reasoning" || signature === "reasoning_text") {
|
|
1918
|
+
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
|
|
1919
|
+
assistantMsg[reasoningField] = allThinkingBlocks.map(b => b.thinking).join("\n");
|
|
1920
|
+
hasReasoningField = true;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
// Tier 2: When the provider requires reasoning_content but there are genuinely no
|
|
1925
|
+
// thinking blocks at all (e.g. proxy stripped reasoning_content from the response),
|
|
1926
|
+
// emit an empty string. The field must be present; an empty string is the most honest
|
|
1927
|
+
// representation of "no reasoning was captured."
|
|
1928
|
+
if (
|
|
1929
|
+
needsReasoningField &&
|
|
1930
|
+
!hasReasoningField &&
|
|
1931
|
+
compat.requiresReasoningContentForToolCalls &&
|
|
1932
|
+
!compat.allowsSyntheticReasoningContentForToolCalls
|
|
1933
|
+
) {
|
|
1934
|
+
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
|
|
1935
|
+
assistantMsg[reasoningField] = "";
|
|
1936
|
+
hasReasoningField = true;
|
|
1937
|
+
}
|
|
1938
|
+
// Tier 3: For providers that accept synthetic placeholders (Kimi, OpenRouter).
|
|
1939
|
+
if (toolCalls.length > 0 && canUseSyntheticReasoningContent && !hasReasoningField) {
|
|
1940
|
+
const reasoningField = compat.reasoningContentField ?? "reasoning_content";
|
|
1941
|
+
assistantMsg[reasoningField] = ".";
|
|
1942
|
+
hasReasoningField = true;
|
|
1943
|
+
}
|
|
1944
|
+
if (toolCalls.length > 0) {
|
|
1945
|
+
assistantMsg.tool_calls = toolCalls.map((tc, toolCallIndex) => {
|
|
1946
|
+
const toolCallId = ensureToolCallId(tc.id, `${i}:${toolCallIndex}:${tc.name}`);
|
|
1947
|
+
rememberToolCallId(tc.id, toolCallId);
|
|
1948
|
+
return {
|
|
1949
|
+
id: normalizeMistralToolId(toolCallId, compat.requiresMistralToolIds),
|
|
1950
|
+
type: "function" as const,
|
|
1951
|
+
function: {
|
|
1952
|
+
name: tc.name,
|
|
1953
|
+
arguments: serializeToolArguments(tc.arguments),
|
|
1954
|
+
},
|
|
1955
|
+
};
|
|
1956
|
+
});
|
|
1957
|
+
const reasoningDetails = toolCalls
|
|
1958
|
+
.filter(tc => tc.thoughtSignature)
|
|
1959
|
+
.map(tc => {
|
|
1960
|
+
try {
|
|
1961
|
+
const parsed: unknown = JSON.parse(tc.thoughtSignature!);
|
|
1962
|
+
return parsed;
|
|
1963
|
+
} catch {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
})
|
|
1967
|
+
.filter(Boolean);
|
|
1968
|
+
if (reasoningDetails.length > 0) {
|
|
1969
|
+
assistantMsg.reasoning_details = reasoningDetails;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
// Some OpenAI-compatible backends concatenate assistant content as a
|
|
1973
|
+
// string even for tool-call replay. OpenAI accepts an empty string here;
|
|
1974
|
+
// null trips strict/proxy implementations before the tool result is read.
|
|
1975
|
+
if (assistantMsg.content === null && (hasReasoningField || assistantMsg.tool_calls)) {
|
|
1976
|
+
assistantMsg.content = "";
|
|
1977
|
+
}
|
|
1978
|
+
// Skip assistant messages that have no content, no tool calls, and no reasoning payload.
|
|
1979
|
+
// Some OpenAI-compatible backends require replaying reasoning-only assistant turns
|
|
1980
|
+
// so follow-up requests preserve the provider-specific reasoning field name.
|
|
1981
|
+
const content = assistantMsg.content;
|
|
1982
|
+
const hasContent =
|
|
1983
|
+
content !== null &&
|
|
1984
|
+
content !== undefined &&
|
|
1985
|
+
(typeof content === "string" ? content.length > 0 : content.length > 0);
|
|
1986
|
+
if (!hasContent && assistantMsg.tool_calls && compat.requiresAssistantContentForToolCalls) {
|
|
1987
|
+
assistantMsg.content = ".";
|
|
1988
|
+
}
|
|
1989
|
+
if (!hasContent && !assistantMsg.tool_calls && !hasReasoningField) {
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
params.push(assistantMsg);
|
|
1993
|
+
} else if (msg.role === "toolResult") {
|
|
1994
|
+
// Batch consecutive tool results and collect all images
|
|
1995
|
+
const imageBlocks: Array<{ type: "image_url"; image_url: { url: string } }> = [];
|
|
1996
|
+
let j = i;
|
|
1997
|
+
|
|
1998
|
+
for (; j < transformedMessages.length && transformedMessages[j].role === "toolResult"; j++) {
|
|
1999
|
+
const toolMsg = transformedMessages[j] as ToolResultMessage;
|
|
2000
|
+
|
|
2001
|
+
// Extract text and image content
|
|
2002
|
+
const textResult = toolMsg.content
|
|
2003
|
+
.filter(c => c.type === "text")
|
|
2004
|
+
.map(c => (c as TextContent).text)
|
|
2005
|
+
.join("\n");
|
|
2006
|
+
const supportsImages = model.input.includes("image") && !isDashscopeCompatibleModeTextOnlyQwen(model);
|
|
2007
|
+
const hasImages = toolMsg.content.some(c => c.type === "image");
|
|
2008
|
+
const omittedImages = hasImages && !supportsImages;
|
|
2009
|
+
|
|
2010
|
+
// Always send tool result with text (or placeholder if only images)
|
|
2011
|
+
const hasText = textResult.length > 0;
|
|
2012
|
+
const remappedToolCallId = consumeToolCallId(toolMsg.toolCallId);
|
|
2013
|
+
const resolvedToolCallId =
|
|
2014
|
+
remappedToolCallId ?? ensureToolCallId(toolMsg.toolCallId, `${j}:${toolMsg.toolName ?? "tool"}`);
|
|
2015
|
+
const toolResultContent = omittedImages
|
|
2016
|
+
? joinTextWithImagePlaceholder(textResult, true)
|
|
2017
|
+
: hasText
|
|
2018
|
+
? textResult
|
|
2019
|
+
: hasImages
|
|
2020
|
+
? "(see attached image)"
|
|
2021
|
+
: "";
|
|
2022
|
+
const toolResultMsg: OpenAICompletionsToolMessageParam = {
|
|
2023
|
+
role: "tool",
|
|
2024
|
+
content: toolResultContent.toWellFormed(),
|
|
2025
|
+
tool_call_id: normalizeMistralToolId(resolvedToolCallId, compat.requiresMistralToolIds),
|
|
2026
|
+
};
|
|
2027
|
+
if (compat.requiresToolResultName && toolMsg.toolName) {
|
|
2028
|
+
toolResultMsg.name = toolMsg.toolName;
|
|
2029
|
+
}
|
|
2030
|
+
params.push(toolResultMsg);
|
|
2031
|
+
|
|
2032
|
+
if (hasImages && supportsImages) {
|
|
2033
|
+
for (const block of toolMsg.content) {
|
|
2034
|
+
if (block.type === "image") {
|
|
2035
|
+
imageBlocks.push({
|
|
2036
|
+
type: "image_url",
|
|
2037
|
+
image_url: {
|
|
2038
|
+
url: `data:${block.mimeType};base64,${block.data}`,
|
|
2039
|
+
},
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
i = j - 1;
|
|
2047
|
+
|
|
2048
|
+
// After all consecutive tool results, add a single user message with all images
|
|
2049
|
+
if (imageBlocks.length > 0) {
|
|
2050
|
+
if (compat.requiresAssistantAfterToolResult) {
|
|
2051
|
+
params.push({
|
|
2052
|
+
role: "assistant",
|
|
2053
|
+
content: "I have processed the tool results.",
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
params.push({
|
|
2058
|
+
role: "user",
|
|
2059
|
+
content: [
|
|
2060
|
+
{
|
|
2061
|
+
type: "text",
|
|
2062
|
+
text: "Attached image(s) from tool result:",
|
|
2063
|
+
},
|
|
2064
|
+
...imageBlocks,
|
|
2065
|
+
],
|
|
2066
|
+
});
|
|
2067
|
+
lastRole = "user";
|
|
2068
|
+
} else {
|
|
2069
|
+
lastRole = "toolResult";
|
|
2070
|
+
}
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
lastRole =
|
|
2075
|
+
msg.role === "developer"
|
|
2076
|
+
? model.reasoning && compat.supportsDeveloperRole
|
|
2077
|
+
? "developer"
|
|
2078
|
+
: "system"
|
|
2079
|
+
: msg.role;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
return params;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function convertTools(
|
|
2086
|
+
tools: Tool[],
|
|
2087
|
+
compat: ResolvedOpenAICompat,
|
|
2088
|
+
toolStrictModeOverride?: ToolStrictModeOverride,
|
|
2089
|
+
): BuiltOpenAICompletionTools {
|
|
2090
|
+
const adaptedTools = tools.map(tool => {
|
|
2091
|
+
const strict = !NO_STRICT && compat.supportsStrictMode !== false && tool.strict !== false;
|
|
2092
|
+
const baseParameters = toolWireSchema(tool);
|
|
2093
|
+
const adapted = adaptSchemaForStrict(baseParameters, strict);
|
|
2094
|
+
return {
|
|
2095
|
+
tool,
|
|
2096
|
+
baseParameters,
|
|
2097
|
+
parameters: adapted.schema,
|
|
2098
|
+
strict: adapted.strict,
|
|
2099
|
+
};
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
const requestedStrictMode = toolStrictModeOverride ?? compat.toolStrictMode;
|
|
2103
|
+
const toolStrictMode =
|
|
2104
|
+
requestedStrictMode === "none"
|
|
2105
|
+
? "none"
|
|
2106
|
+
: requestedStrictMode === "all_strict"
|
|
2107
|
+
? adaptedTools.every(tool => tool.strict)
|
|
2108
|
+
? "all_strict"
|
|
2109
|
+
: "none"
|
|
2110
|
+
: "mixed";
|
|
2111
|
+
|
|
2112
|
+
return {
|
|
2113
|
+
tools: adaptedTools.map(({ tool, baseParameters, parameters, strict }) => {
|
|
2114
|
+
const includeStrict = toolStrictMode === "all_strict" || (toolStrictMode === "mixed" && strict);
|
|
2115
|
+
const wireParameters = includeStrict ? parameters : baseParameters;
|
|
2116
|
+
return {
|
|
2117
|
+
type: "function",
|
|
2118
|
+
function: {
|
|
2119
|
+
name: tool.name,
|
|
2120
|
+
description: tool.description || "",
|
|
2121
|
+
// Moonshot/Kimi native hosts validate against the stricter MFJS subset
|
|
2122
|
+
// (const→enum, typed enums, no validators) and 400 otherwise.
|
|
2123
|
+
parameters:
|
|
2124
|
+
compat.toolSchemaFlavor === "moonshot-mfjs"
|
|
2125
|
+
? (normalizeSchemaForMoonshot(wireParameters) as Record<string, unknown>)
|
|
2126
|
+
: wireParameters,
|
|
2127
|
+
// Only include strict if provider supports it. Some reject unknown fields.
|
|
2128
|
+
...(includeStrict && { strict: true }),
|
|
2129
|
+
},
|
|
2130
|
+
};
|
|
2131
|
+
}),
|
|
2132
|
+
toolStrictMode,
|
|
2133
|
+
strictToolsApplied:
|
|
2134
|
+
tools.length > 0 &&
|
|
2135
|
+
(toolStrictMode === "all_strict" || (toolStrictMode === "mixed" && adaptedTools.some(tool => tool.strict))),
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
|
|
2140
|
+
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
|
|
2141
|
+
|
|
2142
|
+
function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
|
|
2143
|
+
stopReason: StopReason;
|
|
2144
|
+
errorMessage?: string;
|
|
2145
|
+
} {
|
|
2146
|
+
if (reason === null) return { stopReason: "stop" };
|
|
2147
|
+
switch (reason) {
|
|
2148
|
+
case "stop":
|
|
2149
|
+
case "end":
|
|
2150
|
+
return { stopReason: "stop" };
|
|
2151
|
+
case "length":
|
|
2152
|
+
return { stopReason: "length" };
|
|
2153
|
+
case "function_call":
|
|
2154
|
+
case "tool_calls":
|
|
2155
|
+
return { stopReason: "toolUse" };
|
|
2156
|
+
case "content_filter":
|
|
2157
|
+
return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
|
|
2158
|
+
case "network_error":
|
|
2159
|
+
return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
|
|
2160
|
+
case "error":
|
|
2161
|
+
// Gateways (OpenRouter, Vercel AI Gateway, …) report upstream model
|
|
2162
|
+
// failures as a bare `finish_reason: "error"` with no detail. These are
|
|
2163
|
+
// almost always transient (e.g. Gemini MALFORMED_FUNCTION_CALL), so word
|
|
2164
|
+
// the message to match the session retry classifier's transient-transport
|
|
2165
|
+
// pattern (`provider.?returned.?error`) and get the turn auto-retried.
|
|
2166
|
+
return { stopReason: "error", errorMessage: "Provider returned error finish_reason" };
|
|
2167
|
+
default:
|
|
2168
|
+
return {
|
|
2169
|
+
stopReason: "error",
|
|
2170
|
+
errorMessage: `Provider finish_reason: ${reason}`,
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
}
|