@linxiraos/pi-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5066 -0
- package/README.md +1195 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
- package/dist/types/auth-broker/client.d.ts +93 -0
- package/dist/types/auth-broker/discover.d.ts +39 -0
- package/dist/types/auth-broker/index.d.ts +7 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +136 -0
- package/dist/types/auth-broker/server.d.ts +48 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +152 -0
- package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
- package/dist/types/auth-gateway/http.d.ts +56 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +36 -0
- package/dist/types/auth-gateway/types.d.ts +127 -0
- package/dist/types/auth-retry.d.ts +150 -0
- package/dist/types/auth-storage.d.ts +1258 -0
- package/dist/types/dialect/anthropic.d.ts +15 -0
- package/dist/types/dialect/catalog.d.ts +3 -0
- package/dist/types/dialect/coercion.d.ts +23 -0
- package/dist/types/dialect/deepseek.d.ts +14 -0
- package/dist/types/dialect/demotion.d.ts +30 -0
- package/dist/types/dialect/examples.d.ts +18 -0
- package/dist/types/dialect/factory.d.ts +3 -0
- package/dist/types/dialect/fenced-thinking.d.ts +53 -0
- package/dist/types/dialect/gemini.d.ts +17 -0
- package/dist/types/dialect/gemma.d.ts +15 -0
- package/dist/types/dialect/glm.d.ts +9 -0
- package/dist/types/dialect/harmony.d.ts +8 -0
- package/dist/types/dialect/hermes.d.ts +9 -0
- package/dist/types/dialect/history.d.ts +3 -0
- package/dist/types/dialect/index.d.ts +11 -0
- package/dist/types/dialect/inventory.d.ts +9 -0
- package/dist/types/dialect/kimi.d.ts +14 -0
- package/dist/types/dialect/minimax.d.ts +3 -0
- package/dist/types/dialect/owned-stream.d.ts +4 -0
- package/dist/types/dialect/qwen3.d.ts +9 -0
- package/dist/types/dialect/rendering.d.ts +54 -0
- package/dist/types/dialect/thinking.d.ts +6 -0
- package/dist/types/dialect/types.d.ts +68 -0
- package/dist/types/dialect/xml.d.ts +9 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +20 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +27 -0
- package/dist/types/error/classes.d.ts +106 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +94 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +14 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/error/rate-limit.d.ts +80 -0
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/providers/amazon-bedrock.d.ts +39 -0
- package/dist/types/providers/anthropic-client.d.ts +106 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic-wire.d.ts +345 -0
- package/dist/types/providers/anthropic.d.ts +261 -0
- package/dist/types/providers/aws-credentials.d.ts +48 -0
- package/dist/types/providers/aws-eventstream.d.ts +39 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +16 -0
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
- package/dist/types/providers/cowork-fetch.d.ts +3 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor-pi-args.d.ts +105 -0
- package/dist/types/providers/cursor.d.ts +221 -0
- package/dist/types/providers/devin.d.ts +12 -0
- package/dist/types/providers/error-message.d.ts +25 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +120 -0
- package/dist/types/providers/google-shared.d.ts +203 -0
- package/dist/types/providers/google-types.d.ts +155 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +179 -0
- package/dist/types/providers/ollama.d.ts +8 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-chat-wire.d.ts +669 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
- package/dist/types/providers/openai-codex-responses.d.ts +238 -0
- package/dist/types/providers/openai-completions.d.ts +48 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
- package/dist/types/providers/openai-responses.d.ts +131 -0
- package/dist/types/providers/openai-shared.d.ts +624 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +69 -0
- package/dist/types/providers/register-builtins.d.ts +37 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +32 -0
- package/dist/types/providers/vision-guard.d.ts +20 -0
- package/dist/types/registry/aiand.d.ts +7 -0
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
- package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/registry/api-key-login.d.ts +42 -0
- package/dist/types/registry/api-key-validation.d.ts +43 -0
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/azure.d.ts +4 -0
- package/dist/types/registry/baseten.d.ts +7 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/cerebras.d.ts +7 -0
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
- package/dist/types/registry/coreweave.d.ts +7 -0
- package/dist/types/registry/cursor.d.ts +7 -0
- package/dist/types/registry/deepseek.d.ts +8 -0
- package/dist/types/registry/derived.d.ts +5 -0
- package/dist/types/registry/devin.d.ts +8 -0
- package/dist/types/registry/exa.d.ts +8 -0
- package/dist/types/registry/firepass.d.ts +16 -0
- package/dist/types/registry/fireworks.d.ts +7 -0
- package/dist/types/registry/github-copilot.d.ts +7 -0
- package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
- package/dist/types/registry/gitlab-duo.d.ts +9 -0
- package/dist/types/registry/gmi-cloud.d.ts +7 -0
- package/dist/types/registry/google-antigravity.d.ts +9 -0
- package/dist/types/registry/google-gemini-cli.d.ts +9 -0
- package/dist/types/registry/google-vertex.d.ts +5 -0
- package/dist/types/registry/google.d.ts +4 -0
- package/dist/types/registry/groq.d.ts +4 -0
- package/dist/types/registry/huggingface.d.ts +7 -0
- package/dist/types/registry/index.d.ts +4 -0
- package/dist/types/registry/kagi.d.ts +14 -0
- package/dist/types/registry/kilo.d.ts +7 -0
- package/dist/types/registry/kimi-code.d.ts +7 -0
- package/dist/types/registry/litellm.d.ts +13 -0
- package/dist/types/registry/llama-cpp.d.ts +8 -0
- package/dist/types/registry/lm-studio.d.ts +8 -0
- package/dist/types/registry/meta.d.ts +7 -0
- package/dist/types/registry/minimax-code-cn.d.ts +6 -0
- package/dist/types/registry/minimax-code.d.ts +6 -0
- package/dist/types/registry/minimax.d.ts +4 -0
- package/dist/types/registry/mistral.d.ts +4 -0
- package/dist/types/registry/moonshot.d.ts +7 -0
- package/dist/types/registry/nanogpt.d.ts +7 -0
- package/dist/types/registry/novita.d.ts +6 -0
- package/dist/types/registry/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
- package/dist/types/registry/oauth/anthropic.d.ts +24 -0
- package/dist/types/registry/oauth/callback-server.d.ts +74 -0
- package/dist/types/registry/oauth/cursor.d.ts +16 -0
- package/dist/types/registry/oauth/device-code.d.ts +25 -0
- package/dist/types/registry/oauth/devin.d.ts +5 -0
- package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
- package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
- package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
- package/dist/types/registry/oauth/index.d.ts +45 -0
- package/dist/types/registry/oauth/kimi.d.ts +21 -0
- package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
- package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
- package/dist/types/registry/oauth/opencode.d.ts +18 -0
- package/dist/types/registry/oauth/perplexity.d.ts +9 -0
- package/dist/types/registry/oauth/pkce.d.ts +8 -0
- package/dist/types/registry/oauth/types.d.ts +87 -0
- package/dist/types/registry/oauth/wafer.d.ts +1 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
- package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
- package/dist/types/registry/oauth/zai.d.ts +25 -0
- package/dist/types/registry/ollama-cloud.d.ts +7 -0
- package/dist/types/registry/ollama.d.ts +12 -0
- package/dist/types/registry/openai-codex-device.d.ts +8 -0
- package/dist/types/registry/openai-codex.d.ts +9 -0
- package/dist/types/registry/openai.d.ts +4 -0
- package/dist/types/registry/opencode-go.d.ts +6 -0
- package/dist/types/registry/opencode-zen.d.ts +6 -0
- package/dist/types/registry/openrouter.d.ts +13 -0
- package/dist/types/registry/parallel.d.ts +14 -0
- package/dist/types/registry/perplexity.d.ts +7 -0
- package/dist/types/registry/qianfan.d.ts +7 -0
- package/dist/types/registry/qwen-portal.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +367 -0
- package/dist/types/registry/sakana.d.ts +7 -0
- package/dist/types/registry/siliconflow-cn.d.ts +7 -0
- package/dist/types/registry/siliconflow.d.ts +7 -0
- package/dist/types/registry/synthetic.d.ts +6 -0
- package/dist/types/registry/tavily.d.ts +14 -0
- package/dist/types/registry/together.d.ts +6 -0
- package/dist/types/registry/types.d.ts +75 -0
- package/dist/types/registry/umans.d.ts +7 -0
- package/dist/types/registry/venice.d.ts +13 -0
- package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
- package/dist/types/registry/vllm.d.ts +7 -0
- package/dist/types/registry/wafer-serverless.d.ts +6 -0
- package/dist/types/registry/xai-oauth.d.ts +7 -0
- package/dist/types/registry/xai.d.ts +7 -0
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
- package/dist/types/registry/xiaomi.d.ts +6 -0
- package/dist/types/registry/zai.d.ts +15 -0
- package/dist/types/registry/zenmux.d.ts +7 -0
- package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
- package/dist/types/stream.d.ts +46 -0
- package/dist/types/types.d.ts +1064 -0
- package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
- package/dist/types/usage/claude.d.ts +4 -0
- package/dist/types/usage/cursor.d.ts +4 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +15 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +3 -0
- package/dist/types/usage/ollama.d.ts +5 -0
- package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
- package/dist/types/usage/openai-codex-reset.d.ts +88 -0
- package/dist/types/usage/openai-codex.d.ts +10 -0
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/synthetic.d.ts +2 -0
- package/dist/types/usage/umans.d.ts +2 -0
- package/dist/types/usage/xai-oauth.d.ts +12 -0
- package/dist/types/usage/zai.d.ts +3 -0
- package/dist/types/usage.d.ts +527 -0
- package/dist/types/utils/abort.d.ts +25 -0
- package/dist/types/utils/anthropic-auth.d.ts +35 -0
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/block-symbols.d.ts +62 -0
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/empty-completion-retry.d.ts +21 -0
- package/dist/types/utils/event-stream.d.ts +39 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/google-validation.d.ts +2 -0
- package/dist/types/utils/harmony-leak.d.ts +135 -0
- package/dist/types/utils/http-inspector.d.ts +49 -0
- package/dist/types/utils/idle-iterator.d.ts +149 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
- package/dist/types/utils/openai-http.d.ts +48 -0
- package/dist/types/utils/openrouter-headers.d.ts +1 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/proxy.d.ts +39 -0
- package/dist/types/utils/request-debug.d.ts +29 -0
- package/dist/types/utils/retry-after.d.ts +4 -0
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +54 -0
- package/dist/types/utils/schema/index.d.ts +14 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +153 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +17 -0
- package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/typescript.d.ts +24 -0
- package/dist/types/utils/schema/wire.d.ts +52 -0
- package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
- package/dist/types/utils/sse-debug.d.ts +5 -0
- package/dist/types/utils/stream-markup-healing.d.ts +87 -0
- package/dist/types/utils/thinking-loop.d.ts +102 -0
- package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
- package/dist/types/utils/tool-choice.d.ts +52 -0
- package/dist/types/utils/validation.d.ts +28 -0
- package/dist/types/utils.d.ts +57 -0
- package/package.json +138 -0
- package/src/api-registry.ts +109 -0
- package/src/auth/sqlite-credential-store.ts +2066 -0
- package/src/auth-broker/client.ts +471 -0
- package/src/auth-broker/discover.ts +310 -0
- package/src/auth-broker/index.ts +7 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +1332 -0
- package/src/auth-broker/server.ts +898 -0
- package/src/auth-broker/snapshot-cache.ts +200 -0
- package/src/auth-broker/types.ts +193 -0
- package/src/auth-broker/wire-schema-resource.ts +487 -0
- package/src/auth-broker/wire-schemas.ts +43 -0
- package/src/auth-gateway/http.ts +227 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +836 -0
- package/src/auth-gateway/types.ts +153 -0
- package/src/auth-retry.ts +401 -0
- package/src/auth-storage.ts +6540 -0
- package/src/dialect/anthropic.md +31 -0
- package/src/dialect/anthropic.ts +608 -0
- package/src/dialect/catalog.ts +29 -0
- package/src/dialect/coercion.ts +136 -0
- package/src/dialect/deepseek.md +24 -0
- package/src/dialect/deepseek.ts +609 -0
- package/src/dialect/demotion.ts +40 -0
- package/src/dialect/examples.ts +71 -0
- package/src/dialect/factory.ts +34 -0
- package/src/dialect/fenced-thinking.ts +184 -0
- package/src/dialect/gemini.md +44 -0
- package/src/dialect/gemini.ts +583 -0
- package/src/dialect/gemma.md +33 -0
- package/src/dialect/gemma.ts +387 -0
- package/src/dialect/glm.md +32 -0
- package/src/dialect/glm.ts +579 -0
- package/src/dialect/harmony.md +31 -0
- package/src/dialect/harmony.ts +345 -0
- package/src/dialect/hermes.md +25 -0
- package/src/dialect/hermes.ts +206 -0
- package/src/dialect/history.ts +81 -0
- package/src/dialect/index.ts +15 -0
- package/src/dialect/inventory.ts +30 -0
- package/src/dialect/kimi.md +24 -0
- package/src/dialect/kimi.ts +340 -0
- package/src/dialect/minimax.md +31 -0
- package/src/dialect/minimax.ts +95 -0
- package/src/dialect/owned-stream.ts +481 -0
- package/src/dialect/prompt-template.md +12 -0
- package/src/dialect/qwen3.md +28 -0
- package/src/dialect/qwen3.ts +240 -0
- package/src/dialect/rendering.ts +304 -0
- package/src/dialect/thinking.ts +292 -0
- package/src/dialect/types.ts +56 -0
- package/src/dialect/xml.md +22 -0
- package/src/dialect/xml.ts +90 -0
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +47 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +35 -0
- package/src/error/classes.ts +281 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +602 -0
- package/src/error/format.ts +45 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +14 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +63 -0
- package/src/error/rate-limit.ts +303 -0
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +51 -0
- package/src/provider-details.ts +90 -0
- package/src/providers/amazon-bedrock.ts +1064 -0
- package/src/providers/anthropic-client.ts +317 -0
- package/src/providers/anthropic-messages-server-schema.ts +252 -0
- package/src/providers/anthropic-messages-server.ts +818 -0
- package/src/providers/anthropic-wire.ts +359 -0
- package/src/providers/anthropic.ts +4539 -0
- package/src/providers/aws-credentials.ts +772 -0
- package/src/providers/aws-eventstream.ts +181 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +438 -0
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/claude-code-fingerprint.ts +20 -0
- package/src/providers/cowork-fetch.ts +201 -0
- package/src/providers/cursor/exec-modern.ts +496 -0
- package/src/providers/cursor/proto/agent.proto +4533 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor-pi-args.ts +165 -0
- package/src/providers/cursor.ts +4689 -0
- package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
- package/src/providers/devin/proto/buf.gen.yaml +33 -0
- package/src/providers/devin/proto/buf.yaml +17 -0
- package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
- package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
- package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
- package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
- package/src/providers/devin/proto/cel/expr/value.proto +41 -0
- package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
- package/src/providers/devin/proto/errorspb/errors.proto +56 -0
- package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
- package/src/providers/devin/proto/errorspb/markers.proto +10 -0
- package/src/providers/devin/proto/errorspb/tags.proto +12 -0
- package/src/providers/devin/proto/errorspb/testing.proto +6 -0
- package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
- package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
- package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
- package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
- package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
- package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
- package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
- package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
- package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
- package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
- package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
- package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
- package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
- package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
- package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
- package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
- package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
- package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
- package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
- package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
- package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
- package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
- package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
- package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
- package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
- package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
- package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
- package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
- package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
- package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
- package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
- package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
- package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
- package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
- package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
- package/src/providers/devin.ts +679 -0
- package/src/providers/error-message.ts +23 -0
- package/src/providers/github-copilot-headers.ts +141 -0
- package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
- package/src/providers/gitlab-duo-workflow.ts +3135 -0
- package/src/providers/gitlab-duo.ts +399 -0
- package/src/providers/google-auth.ts +330 -0
- package/src/providers/google-gemini-cli.ts +1370 -0
- package/src/providers/google-shared.ts +1122 -0
- package/src/providers/google-types.ts +180 -0
- package/src/providers/google-vertex.ts +135 -0
- package/src/providers/google.ts +47 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +51 -0
- package/src/providers/mock.ts +514 -0
- package/src/providers/ollama.ts +776 -0
- package/src/providers/openai-anthropic-shim.ts +166 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +752 -0
- package/src/providers/openai-chat-wire.ts +859 -0
- package/src/providers/openai-codex/request-transformer.ts +491 -0
- package/src/providers/openai-codex/response-handler.ts +102 -0
- package/src/providers/openai-codex-responses.ts +4716 -0
- package/src/providers/openai-completions.ts +2389 -0
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses-server-schema.ts +397 -0
- package/src/providers/openai-responses-server.ts +1466 -0
- package/src/providers/openai-responses-wire.ts +6416 -0
- package/src/providers/openai-responses.ts +1393 -0
- package/src/providers/openai-shared.ts +3500 -0
- package/src/providers/pi-native-client.ts +275 -0
- package/src/providers/pi-native-server.ts +245 -0
- package/src/providers/register-builtins.ts +503 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +1083 -0
- package/src/providers/vision-guard.ts +54 -0
- package/src/registry/aiand.ts +22 -0
- package/src/registry/aimlapi.ts +6 -0
- package/src/registry/alibaba-coding-plan.ts +104 -0
- package/src/registry/alibaba-token-plan.ts +125 -0
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/registry/api-key-login.ts +115 -0
- package/src/registry/api-key-validation.ts +145 -0
- package/src/registry/aws.ts +57 -0
- package/src/registry/azure.ts +6 -0
- package/src/registry/baseten.ts +22 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/cerebras.ts +23 -0
- package/src/registry/cloudflare-ai-gateway.ts +45 -0
- package/src/registry/coreweave.ts +40 -0
- package/src/registry/cursor.ts +20 -0
- package/src/registry/deepseek.ts +46 -0
- package/src/registry/derived.ts +9 -0
- package/src/registry/devin.ts +15 -0
- package/src/registry/exa.ts +19 -0
- package/src/registry/firepass.ts +32 -0
- package/src/registry/fireworks.ts +28 -0
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo-workflow.ts +20 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/gmi-cloud.ts +22 -0
- package/src/registry/google-antigravity.ts +22 -0
- package/src/registry/google-gemini-cli.ts +22 -0
- package/src/registry/google-vertex.ts +38 -0
- package/src/registry/google.ts +6 -0
- package/src/registry/groq.ts +6 -0
- package/src/registry/huggingface.ts +29 -0
- package/src/registry/index.ts +4 -0
- package/src/registry/kagi.ts +46 -0
- package/src/registry/kilo.ts +114 -0
- package/src/registry/kimi-code.ts +17 -0
- package/src/registry/litellm.ts +45 -0
- package/src/registry/llama-cpp.ts +35 -0
- package/src/registry/lm-studio.ts +31 -0
- package/src/registry/meta.ts +22 -0
- package/src/registry/minimax-code-cn.ts +12 -0
- package/src/registry/minimax-code.ts +12 -0
- package/src/registry/minimax.ts +6 -0
- package/src/registry/mistral.ts +6 -0
- package/src/registry/moonshot.ts +28 -0
- package/src/registry/nanogpt.ts +22 -0
- package/src/registry/novita.ts +25 -0
- package/src/registry/nvidia.ts +61 -0
- package/src/registry/oauth/anthropic-constants.ts +12 -0
- package/src/registry/oauth/anthropic.ts +346 -0
- package/src/registry/oauth/callback-server.ts +438 -0
- package/src/registry/oauth/cursor.ts +187 -0
- package/src/registry/oauth/device-code.ts +92 -0
- package/src/registry/oauth/devin.ts +124 -0
- package/src/registry/oauth/github-copilot.ts +369 -0
- package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
- package/src/registry/oauth/gitlab-duo.ts +222 -0
- package/src/registry/oauth/google-antigravity.ts +225 -0
- package/src/registry/oauth/google-gemini-cli.ts +297 -0
- package/src/registry/oauth/google-oauth-shared.ts +211 -0
- package/src/registry/oauth/index.ts +187 -0
- package/src/registry/oauth/kimi.ts +297 -0
- package/src/registry/oauth/minimax-code.ts +53 -0
- package/src/registry/oauth/oauth.html +317 -0
- package/src/registry/oauth/openai-codex.ts +384 -0
- package/src/registry/oauth/opencode.ts +50 -0
- package/src/registry/oauth/perplexity.ts +228 -0
- package/src/registry/oauth/pkce.ts +18 -0
- package/src/registry/oauth/types.ts +96 -0
- package/src/registry/oauth/wafer.ts +24 -0
- package/src/registry/oauth/xai-oauth.ts +559 -0
- package/src/registry/oauth/xiaomi.ts +211 -0
- package/src/registry/oauth/zai.ts +285 -0
- package/src/registry/ollama-cloud.ts +36 -0
- package/src/registry/ollama.ts +43 -0
- package/src/registry/openai-codex-device.ts +18 -0
- package/src/registry/openai-codex.ts +19 -0
- package/src/registry/openai.ts +6 -0
- package/src/registry/opencode-go.ts +12 -0
- package/src/registry/opencode-zen.ts +12 -0
- package/src/registry/openrouter.ts +28 -0
- package/src/registry/parallel.ts +45 -0
- package/src/registry/perplexity.ts +13 -0
- package/src/registry/qianfan.ts +27 -0
- package/src/registry/qwen-portal.ts +50 -0
- package/src/registry/registry.ts +182 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/siliconflow-cn.ts +22 -0
- package/src/registry/siliconflow.ts +22 -0
- package/src/registry/synthetic.ts +21 -0
- package/src/registry/tavily.ts +45 -0
- package/src/registry/together.ts +22 -0
- package/src/registry/types.ts +86 -0
- package/src/registry/umans.ts +23 -0
- package/src/registry/venice.ts +33 -0
- package/src/registry/vercel-ai-gateway.ts +38 -0
- package/src/registry/vllm.ts +34 -0
- package/src/registry/wafer-serverless.ts +12 -0
- package/src/registry/xai-oauth.ts +17 -0
- package/src/registry/xai.ts +22 -0
- package/src/registry/xiaomi-token-plan-ams.ts +12 -0
- package/src/registry/xiaomi-token-plan-cn.ts +12 -0
- package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
- package/src/registry/xiaomi.ts +12 -0
- package/src/registry/zai.ts +41 -0
- package/src/registry/zenmux.ts +22 -0
- package/src/registry/zhipu-coding-plan.ts +27 -0
- package/src/stream.ts +1944 -0
- package/src/types.ts +1243 -0
- package/src/usage/alibaba-token-plan.ts +230 -0
- package/src/usage/claude.ts +830 -0
- package/src/usage/cursor.ts +335 -0
- package/src/usage/gemini.ts +258 -0
- package/src/usage/github-copilot.ts +424 -0
- package/src/usage/google-antigravity.ts +497 -0
- package/src/usage/kimi.ts +277 -0
- package/src/usage/minimax-code.ts +291 -0
- package/src/usage/ollama.ts +41 -0
- package/src/usage/openai-codex-base-url.ts +35 -0
- package/src/usage/openai-codex-reset.ts +205 -0
- package/src/usage/openai-codex.ts +627 -0
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/synthetic.ts +180 -0
- package/src/usage/umans.ts +192 -0
- package/src/usage/xai-oauth.ts +414 -0
- package/src/usage/zai.ts +370 -0
- package/src/usage.ts +411 -0
- package/src/utils/abort.ts +67 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/block-symbols.ts +78 -0
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/empty-completion-retry.ts +161 -0
- package/src/utils/event-stream.ts +202 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/google-validation.ts +25 -0
- package/src/utils/harmony-leak.ts +500 -0
- package/src/utils/http-inspector.ts +196 -0
- package/src/utils/idle-iterator.ts +531 -0
- package/src/utils/leaked-thinking-stream.ts +483 -0
- package/src/utils/openai-http.ts +119 -0
- package/src/utils/openrouter-headers.ts +12 -0
- package/src/utils/parse-bind.ts +56 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/proxy.ts +314 -0
- package/src/utils/request-debug.ts +351 -0
- package/src/utils/retry-after.ts +121 -0
- package/src/utils/retry.ts +77 -0
- package/src/utils/schema/CONSTRAINTS.md +168 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +210 -0
- package/src/utils/schema/index.ts +14 -0
- package/src/utils/schema/json-schema-validator.ts +595 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +2314 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +109 -0
- package/src/utils/schema/strict-tool-validation.ts +117 -0
- package/src/utils/schema/types.ts +10 -0
- package/src/utils/schema/typescript.ts +212 -0
- package/src/utils/schema/wire.ts +662 -0
- package/src/utils/sdk-stream-timeout.ts +43 -0
- package/src/utils/sse-debug.ts +18 -0
- package/src/utils/stream-markup-healing.ts +247 -0
- package/src/utils/thinking-loop.ts +552 -0
- package/src/utils/tool-call-loop-guard.ts +107 -0
- package/src/utils/tool-choice.ts +101 -0
- package/src/utils/validation.ts +1932 -0
- package/src/utils.ts +492 -0
package/src/stream.ts
ADDED
|
@@ -0,0 +1,1944 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { scheduler } from "node:timers/promises";
|
|
6
|
+
import { isOfficialAnthropicApiUrl } from "@linxiraos/pi-catalog/compat/anthropic";
|
|
7
|
+
import type { Effort } from "@linxiraos/pi-catalog/effort";
|
|
8
|
+
import {
|
|
9
|
+
isVertexExpressOpenAIUrl,
|
|
10
|
+
isVertexRawPredictUrl,
|
|
11
|
+
resolveVertexEndpointHost,
|
|
12
|
+
} from "@linxiraos/pi-catalog/hosts";
|
|
13
|
+
import {
|
|
14
|
+
mapEffortToAnthropicAdaptiveEffort,
|
|
15
|
+
mapEffortToGoogleThinkingLevel,
|
|
16
|
+
minimumSupportedEffort,
|
|
17
|
+
requireSupportedEffort,
|
|
18
|
+
resolveWireModelId,
|
|
19
|
+
} from "@linxiraos/pi-catalog/model-thinking";
|
|
20
|
+
import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@linxiraos/pi-catalog/provider-models";
|
|
21
|
+
import { CODEX_BASE_URL } from "@linxiraos/pi-catalog/wire/codex";
|
|
22
|
+
import { $env, $pickenv, getProviderInFlightRoot, isEnoent, logger, withExtraCaFetch } from "@linxiraos/pi-utils";
|
|
23
|
+
import { getCustomApi } from "./api-registry";
|
|
24
|
+
import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry";
|
|
25
|
+
import * as AIError from "./error";
|
|
26
|
+
import { ProviderHttpError } from "./error";
|
|
27
|
+
import { isInvalidatedOAuthTokenError } from "./error/auth-classify";
|
|
28
|
+
import { isConcurrencyCapExclusion, isUsageLimitOutcome } from "./error/rate-limit";
|
|
29
|
+
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
30
|
+
import type { AnthropicOptions } from "./providers/anthropic";
|
|
31
|
+
import { coworkFetch } from "./providers/cowork-fetch";
|
|
32
|
+
import type { CursorOptions } from "./providers/cursor";
|
|
33
|
+
import type { DevinOptions } from "./providers/devin";
|
|
34
|
+
import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
|
|
35
|
+
import { type GitLabDuoWorkflowOptions, streamGitLabDuoWorkflow } from "./providers/gitlab-duo-workflow";
|
|
36
|
+
import type { GoogleOptions } from "./providers/google";
|
|
37
|
+
import { getVertexAccessToken } from "./providers/google-auth";
|
|
38
|
+
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
|
|
39
|
+
import type { GoogleVertexOptions } from "./providers/google-vertex";
|
|
40
|
+
import { isKimiModel, streamKimi } from "./providers/kimi";
|
|
41
|
+
import type { OllamaChatOptions } from "./providers/ollama";
|
|
42
|
+
import type { OpenAICompletionsOptions } from "./providers/openai-completions";
|
|
43
|
+
import { streamPiNative } from "./providers/pi-native-client";
|
|
44
|
+
// Heavy provider stream functions are imported lazily via register-builtins,
|
|
45
|
+
// which wraps each provider module in a dynamic import. This keeps the
|
|
46
|
+
// AWS SDK, google-auth-library, @google/genai, @bufbuild/protobuf, and
|
|
47
|
+
// other provider SDKs out of the CLI startup parse graph. The
|
|
48
|
+
// gitlab-duo / kimi / synthetic providers stay eager because their modules
|
|
49
|
+
// export routing predicates (isGitLabDuoModel, isKimiModel, isSyntheticModel)
|
|
50
|
+
// that must be callable synchronously before streaming begins, and their
|
|
51
|
+
// modules are thin wrappers with no heavy SDK dependencies.
|
|
52
|
+
import {
|
|
53
|
+
streamAnthropic,
|
|
54
|
+
streamAzureOpenAIResponses,
|
|
55
|
+
streamBedrock,
|
|
56
|
+
streamCursor,
|
|
57
|
+
streamDevin,
|
|
58
|
+
streamGoogle,
|
|
59
|
+
streamGoogleGeminiCli,
|
|
60
|
+
streamGoogleVertex,
|
|
61
|
+
streamOllama,
|
|
62
|
+
streamOpenAICodexResponses,
|
|
63
|
+
streamOpenAICompletions,
|
|
64
|
+
streamOpenAIResponses,
|
|
65
|
+
} from "./providers/register-builtins";
|
|
66
|
+
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
67
|
+
import { getProviderDefinition, PROVIDER_REGISTRY } from "./registry";
|
|
68
|
+
import type {
|
|
69
|
+
Api,
|
|
70
|
+
AssistantMessage,
|
|
71
|
+
AssistantMessageEvent,
|
|
72
|
+
Context,
|
|
73
|
+
FetchImpl,
|
|
74
|
+
Model,
|
|
75
|
+
OptionsForApi,
|
|
76
|
+
SimpleStreamOptions,
|
|
77
|
+
StreamOptions,
|
|
78
|
+
ThinkingBudgets,
|
|
79
|
+
ToolChoice,
|
|
80
|
+
} from "./types";
|
|
81
|
+
import { resolveCacheRetention } from "./utils";
|
|
82
|
+
import { AssistantMessageEventStream } from "./utils/event-stream";
|
|
83
|
+
import { isFoundryEnabled } from "./utils/foundry";
|
|
84
|
+
import { wrapLeakedThinkingStream } from "./utils/leaked-thinking-stream";
|
|
85
|
+
import { wrapFetchForProxy } from "./utils/proxy";
|
|
86
|
+
import { withRequestDebugFetch } from "./utils/request-debug";
|
|
87
|
+
import { withGeminiThinkingLoopGuard } from "./utils/thinking-loop";
|
|
88
|
+
|
|
89
|
+
function defaultFetchForModel(model: Model<Api>): FetchImpl {
|
|
90
|
+
if (model.provider === "anthropic" && model.api === "anthropic-messages") return coworkFetch;
|
|
91
|
+
return globalThis.fetch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isGoogleVertexAuthenticatedModel(model: Model<Api>): boolean {
|
|
95
|
+
return (
|
|
96
|
+
model.provider === "google-vertex" &&
|
|
97
|
+
((model.api === "openai-completions" && isVertexExpressOpenAIUrl(model.baseUrl)) ||
|
|
98
|
+
(model.api === "anthropic-messages" && isVertexRawPredictUrl(model.baseUrl)))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether {@link model} is an official first-party endpoint whose stream needs
|
|
104
|
+
* no leaked-thinking healing — the official Anthropic API and the official
|
|
105
|
+
* OpenAI / OpenAI-Codex endpoints return structured thinking blocks and never
|
|
106
|
+
* leak reasoning idioms into the visible text channel.
|
|
107
|
+
*
|
|
108
|
+
* The gate is provider id **and** official endpoint URL: pointing
|
|
109
|
+
* `provider: "anthropic"` (or `openai`) at a custom proxy via `models.yml`
|
|
110
|
+
* still routes through {@link wrapLeakedThinkingStream}, since a third-party
|
|
111
|
+
* gateway may well leak. URL checks are strict (exact origin / path boundary
|
|
112
|
+
* or parsed hostname) — a substring match would accept lookalikes like
|
|
113
|
+
* `https://api.openai.com.evil/`. Anthropic Foundry (`CLAUDE_CODE_USE_FOUNDRY`)
|
|
114
|
+
* redirects an empty `baseUrl` to `FOUNDRY_BASE_URL`, so the check runs against
|
|
115
|
+
* that effective endpoint — exempt only when it resolves to the official host.
|
|
116
|
+
*/
|
|
117
|
+
function isLeakedThinkingHealExempt(model: Model<Api>): boolean {
|
|
118
|
+
switch (model.provider) {
|
|
119
|
+
case "anthropic": {
|
|
120
|
+
// Mirror resolveAnthropicBaseUrl's effective endpoint: Foundry redirects
|
|
121
|
+
// an empty baseUrl to FOUNDRY_BASE_URL; otherwise an explicit non-official
|
|
122
|
+
// model.baseUrl wins, then the ANTHROPIC_BASE_URL gateway fallback, then
|
|
123
|
+
// the official default. Exempt only when the effective endpoint is official.
|
|
124
|
+
if (isFoundryEnabled()) {
|
|
125
|
+
const foundry = $env.FOUNDRY_BASE_URL?.trim();
|
|
126
|
+
if (foundry) return isOfficialAnthropicApiUrl(foundry);
|
|
127
|
+
}
|
|
128
|
+
if (model.baseUrl && !isOfficialAnthropicApiUrl(model.baseUrl)) return false;
|
|
129
|
+
return isOfficialAnthropicApiUrl($env.ANTHROPIC_BASE_URL?.trim() || model.baseUrl);
|
|
130
|
+
}
|
|
131
|
+
case "openai":
|
|
132
|
+
return isOfficialOpenAIApiUrl(model.baseUrl);
|
|
133
|
+
case "openai-codex":
|
|
134
|
+
return isOfficialCodexApiUrl(model.baseUrl);
|
|
135
|
+
default:
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Strict official-OpenAI endpoint check; missing baseUrl defaults to `api.openai.com`. */
|
|
141
|
+
function isOfficialOpenAIApiUrl(baseUrl: string | undefined): boolean {
|
|
142
|
+
if (!baseUrl) return true;
|
|
143
|
+
try {
|
|
144
|
+
return new URL(baseUrl).hostname === "api.openai.com";
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Strict official-Codex endpoint check; exact origin or a path boundary after {@link CODEX_BASE_URL}. */
|
|
151
|
+
export function isOfficialCodexApiUrl(baseUrl: string | undefined): boolean {
|
|
152
|
+
if (!baseUrl) return true;
|
|
153
|
+
const lower = baseUrl.toLowerCase().replace(/\/+$/, "");
|
|
154
|
+
return lower === CODEX_BASE_URL || lower.startsWith(`${CODEX_BASE_URL}/`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Apply live leaked-thinking healing unless {@link model} is an official
|
|
159
|
+
* first-party endpoint ({@link isLeakedThinkingHealExempt}), which emits
|
|
160
|
+
* structured thinking and needs no healing.
|
|
161
|
+
*/
|
|
162
|
+
function healLeakedThinking(model: Model<Api>, inner: AssistantMessageEventStream): AssistantMessageEventStream {
|
|
163
|
+
return isLeakedThinkingHealExempt(model) ? inner : wrapLeakedThinkingStream(inner);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type ProviderInFlightLease = {
|
|
167
|
+
path: string;
|
|
168
|
+
heartbeat: NodeJS.Timeout;
|
|
169
|
+
flushHeartbeat: () => Promise<void>;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type ProviderInFlightLeaseInfo = {
|
|
173
|
+
pid: number;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
token: string;
|
|
176
|
+
};
|
|
177
|
+
type ProviderInFlightStaleLock = { token: string } | { mtimeMs: number };
|
|
178
|
+
type ProviderInFlightLockIdentity = { dev: number; ino: number; birthtimeMs: number };
|
|
179
|
+
|
|
180
|
+
const PROVIDER_INFLIGHT_LOCK_STALE_MS = 10_000;
|
|
181
|
+
const PROVIDER_INFLIGHT_LEASE_STALE_MS = 30_000;
|
|
182
|
+
const PROVIDER_INFLIGHT_HEARTBEAT_MS = 5_000;
|
|
183
|
+
const PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS = 250;
|
|
184
|
+
|
|
185
|
+
let configuredProviderMaxInFlightRequests: Record<string, number> = {};
|
|
186
|
+
let providerInFlightRootOverride: string | undefined;
|
|
187
|
+
|
|
188
|
+
export function configureProviderMaxInFlightRequests(limits: Record<string, number> | undefined): void {
|
|
189
|
+
configuredProviderMaxInFlightRequests = limits ?? {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveProviderInFlightLimit(
|
|
193
|
+
provider: string,
|
|
194
|
+
options?: Pick<StreamOptions, "maxInFlightRequests">,
|
|
195
|
+
): number | undefined {
|
|
196
|
+
const limits = options?.maxInFlightRequests ?? configuredProviderMaxInFlightRequests;
|
|
197
|
+
const value = limits[provider];
|
|
198
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined;
|
|
199
|
+
return Math.max(1, Math.floor(value));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function providerInFlightRoot(): string {
|
|
203
|
+
if (providerInFlightRootOverride) return providerInFlightRootOverride;
|
|
204
|
+
return getProviderInFlightRoot();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function providerInFlightSegment(provider: string): string {
|
|
208
|
+
return crypto.createHash("sha256").update(provider).digest("base64url");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function providerInFlightDir(provider: string): string {
|
|
212
|
+
return path.join(providerInFlightRoot(), providerInFlightSegment(provider));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function providerInFlightSignalPath(provider: string): string {
|
|
216
|
+
return path.join(providerInFlightDir(provider), ".wakeup");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function providerInFlightLockDir(provider: string): string {
|
|
220
|
+
return `${providerInFlightDir(provider)}.lock`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// `process.kill(pid, 0)` may throw for permission/sandbox reasons even when a
|
|
224
|
+
// process exists. Treat non-ESRCH failures as alive; timestamp expiry still
|
|
225
|
+
// reaps leases whose heartbeat stopped.
|
|
226
|
+
function isProcessAlive(pid: number): boolean {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(pid, 0);
|
|
229
|
+
return true;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readProviderInFlightInfo(infoPath: string): Promise<ProviderInFlightLeaseInfo | null> {
|
|
236
|
+
try {
|
|
237
|
+
const content = await fs.readFile(infoPath, "utf-8");
|
|
238
|
+
const parsed = JSON.parse(content) as Partial<ProviderInFlightLeaseInfo>;
|
|
239
|
+
if (typeof parsed.pid !== "number" || typeof parsed.timestamp !== "number" || typeof parsed.token !== "string") {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return { pid: parsed.pid, timestamp: parsed.timestamp, token: parsed.token };
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function writeProviderInFlightInfo(dir: string, token: string): Promise<void> {
|
|
249
|
+
const info: ProviderInFlightLeaseInfo = { pid: process.pid, timestamp: Date.now(), token };
|
|
250
|
+
const infoPath = path.join(dir, "info.json");
|
|
251
|
+
const tempPath = path.join(dir, `.info-${process.pid}-${crypto.randomUUID()}.tmp`);
|
|
252
|
+
try {
|
|
253
|
+
await Bun.write(tempPath, JSON.stringify(info));
|
|
254
|
+
await fs.rename(tempPath, infoPath);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
await fs.rm(tempPath, { force: true }).catch(() => {});
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function isProviderInFlightDirStale(dir: string, staleMs: number): Promise<boolean> {
|
|
262
|
+
const info = await readProviderInFlightInfo(path.join(dir, "info.json"));
|
|
263
|
+
if (info) {
|
|
264
|
+
if (!isProcessAlive(info.pid)) return true;
|
|
265
|
+
return Date.now() - info.timestamp > staleMs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const stat = await fs.stat(path.join(dir, "info.json"));
|
|
270
|
+
return Date.now() - stat.mtimeMs > staleMs;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (!isEnoent(error)) throw error;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const stat = await fs.stat(dir);
|
|
277
|
+
return Date.now() - stat.mtimeMs > staleMs;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (isEnoent(error)) return false;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function readProviderInFlightStaleLock(lockDir: string): Promise<ProviderInFlightStaleLock | null> {
|
|
285
|
+
const infoPath = path.join(lockDir, "info.json");
|
|
286
|
+
const info = await readProviderInFlightInfo(infoPath);
|
|
287
|
+
if (info) return isProcessAlive(info.pid) ? null : { token: info.token };
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const stat = await fs.stat(lockDir);
|
|
291
|
+
return Date.now() - stat.mtimeMs > PROVIDER_INFLIGHT_LOCK_STALE_MS ? { mtimeMs: stat.mtimeMs } : null;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (isEnoent(error)) return null;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function readProviderInFlightLockIdentity(lockDir: string): Promise<ProviderInFlightLockIdentity> {
|
|
299
|
+
const stat = await fs.stat(lockDir);
|
|
300
|
+
return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isSameProviderInFlightLock(
|
|
304
|
+
current: ProviderInFlightLockIdentity,
|
|
305
|
+
expected: ProviderInFlightLockIdentity,
|
|
306
|
+
): boolean {
|
|
307
|
+
if (current.dev !== expected.dev) return false;
|
|
308
|
+
if (current.ino !== 0 || expected.ino !== 0) return current.ino === expected.ino;
|
|
309
|
+
return current.birthtimeMs === expected.birthtimeMs;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function releaseProviderInFlightStaleLock(lockDir: string, stale: ProviderInFlightStaleLock): Promise<void> {
|
|
313
|
+
if ("token" in stale) {
|
|
314
|
+
await releaseProviderInFlightLock(lockDir, stale.token);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const infoPath = path.join(lockDir, "info.json");
|
|
319
|
+
if (await readProviderInFlightInfo(infoPath)) return;
|
|
320
|
+
try {
|
|
321
|
+
const stat = await fs.stat(lockDir);
|
|
322
|
+
if (stat.mtimeMs !== stale.mtimeMs || Date.now() - stat.mtimeMs <= PROVIDER_INFLIGHT_LOCK_STALE_MS) return;
|
|
323
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Best-effort token-checked release. A token mismatch means another process has
|
|
328
|
+
// already replaced the lock, so the fresh lock must be left intact.
|
|
329
|
+
async function releaseProviderInFlightLock(lockDir: string, token: string): Promise<void> {
|
|
330
|
+
try {
|
|
331
|
+
const info = await readProviderInFlightInfo(path.join(lockDir, "info.json"));
|
|
332
|
+
if (!info || info.token !== token) return;
|
|
333
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function releaseProviderInFlightLockDirIfSame(
|
|
338
|
+
lockDir: string,
|
|
339
|
+
identity: ProviderInFlightLockIdentity,
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
try {
|
|
342
|
+
if (await readProviderInFlightInfo(path.join(lockDir, "info.json"))) return;
|
|
343
|
+
const current = await readProviderInFlightLockIdentity(lockDir);
|
|
344
|
+
if (!isSameProviderInFlightLock(current, identity)) return;
|
|
345
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
346
|
+
} catch {}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function acquireProviderInFlightLock(provider: string, signal?: AbortSignal): Promise<() => Promise<void>> {
|
|
350
|
+
const lockDir = providerInFlightLockDir(provider);
|
|
351
|
+
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
352
|
+
|
|
353
|
+
while (true) {
|
|
354
|
+
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
355
|
+
try {
|
|
356
|
+
await fs.mkdir(lockDir);
|
|
357
|
+
const lockIdentity = await readProviderInFlightLockIdentity(lockDir);
|
|
358
|
+
const token = crypto.randomUUID();
|
|
359
|
+
try {
|
|
360
|
+
await writeProviderInFlightInfo(lockDir, token);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
await releaseProviderInFlightLockDirIfSame(lockDir, lockIdentity);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
return async () => {
|
|
366
|
+
await releaseProviderInFlightLock(lockDir, token);
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const staleLock = await readProviderInFlightStaleLock(lockDir);
|
|
373
|
+
if (staleLock) {
|
|
374
|
+
await releaseProviderInFlightStaleLock(lockDir, staleLock);
|
|
375
|
+
await signalProviderInFlightWaiters(provider);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await waitForProviderInFlightSignal(provider, signal);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function cleanupProviderInFlightLeases(providerDir: string): Promise<number> {
|
|
384
|
+
let active = 0;
|
|
385
|
+
let entries: string[];
|
|
386
|
+
try {
|
|
387
|
+
entries = await fs.readdir(providerDir);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (isEnoent(error)) return 0;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const entry of entries) {
|
|
394
|
+
const leaseDir = path.join(providerDir, entry);
|
|
395
|
+
let isDirectory = false;
|
|
396
|
+
try {
|
|
397
|
+
isDirectory = (await fs.stat(leaseDir)).isDirectory();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isEnoent(error)) continue;
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
if (!isDirectory) continue;
|
|
403
|
+
if (await isProviderInFlightDirStale(leaseDir, PROVIDER_INFLIGHT_LEASE_STALE_MS)) {
|
|
404
|
+
await fs.rm(leaseDir, { recursive: true, force: true });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
active++;
|
|
408
|
+
}
|
|
409
|
+
return active;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function tryAcquireProviderInFlightLease(
|
|
413
|
+
provider: string,
|
|
414
|
+
limit: number,
|
|
415
|
+
signal?: AbortSignal,
|
|
416
|
+
): Promise<ProviderInFlightLease | null> {
|
|
417
|
+
const releaseLock = await acquireProviderInFlightLock(provider, signal);
|
|
418
|
+
try {
|
|
419
|
+
const dir = providerInFlightDir(provider);
|
|
420
|
+
await fs.mkdir(dir, { recursive: true });
|
|
421
|
+
const active = await cleanupProviderInFlightLeases(dir);
|
|
422
|
+
if (active >= limit) return null;
|
|
423
|
+
|
|
424
|
+
const leaseDir = path.join(dir, `${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
|
|
425
|
+
const token = crypto.randomUUID();
|
|
426
|
+
try {
|
|
427
|
+
await fs.mkdir(leaseDir);
|
|
428
|
+
await writeProviderInFlightInfo(leaseDir, token);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
await removeProviderInFlightLeaseDir(leaseDir).catch(() => {});
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
let heartbeatFlush = Promise.resolve();
|
|
434
|
+
const touchHeartbeat = () => {
|
|
435
|
+
heartbeatFlush = heartbeatFlush
|
|
436
|
+
.then(
|
|
437
|
+
() => writeProviderInFlightInfo(leaseDir, token),
|
|
438
|
+
() => writeProviderInFlightInfo(leaseDir, token),
|
|
439
|
+
)
|
|
440
|
+
.catch(() => {});
|
|
441
|
+
};
|
|
442
|
+
const heartbeat = setInterval(touchHeartbeat, PROVIDER_INFLIGHT_HEARTBEAT_MS);
|
|
443
|
+
heartbeat.unref?.();
|
|
444
|
+
return { path: leaseDir, heartbeat, flushHeartbeat: () => heartbeatFlush };
|
|
445
|
+
} finally {
|
|
446
|
+
await releaseLock();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function signalProviderInFlightWaitersInDir(dir: string): Promise<void> {
|
|
451
|
+
try {
|
|
452
|
+
await fs.mkdir(dir, { recursive: true });
|
|
453
|
+
await Bun.write(path.join(dir, ".wakeup"), String(Date.now()));
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function signalProviderInFlightWaiters(provider: string): Promise<void> {
|
|
458
|
+
await signalProviderInFlightWaitersInDir(providerInFlightDir(provider));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal): Promise<void> {
|
|
462
|
+
if (signal?.aborted)
|
|
463
|
+
return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
|
|
464
|
+
const signalPath = providerInFlightSignalPath(provider);
|
|
465
|
+
const waitStarted = Date.now();
|
|
466
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
467
|
+
let settled = false;
|
|
468
|
+
let watcher: fsSync.FSWatcher | undefined;
|
|
469
|
+
const timer = setTimeout(() => finish(resolve), PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS);
|
|
470
|
+
const finish = (settle: () => void) => {
|
|
471
|
+
if (settled) return;
|
|
472
|
+
settled = true;
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
watcher?.close();
|
|
475
|
+
signal?.removeEventListener("abort", onAbort);
|
|
476
|
+
settle();
|
|
477
|
+
};
|
|
478
|
+
const onAbort = () => {
|
|
479
|
+
finish(() => reject(signal?.reason ?? new AIError.AbortError("Provider request aborted before dispatch")));
|
|
480
|
+
};
|
|
481
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
482
|
+
try {
|
|
483
|
+
watcher = fsSync.watch(providerInFlightDir(provider), (_event, filename) => {
|
|
484
|
+
if (filename === ".wakeup" || filename === null) {
|
|
485
|
+
finish(resolve);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
void fs.stat(signalPath).then(
|
|
489
|
+
stat => {
|
|
490
|
+
if (stat.mtimeMs >= waitStarted) finish(resolve);
|
|
491
|
+
},
|
|
492
|
+
error => {
|
|
493
|
+
if (!isEnoent(error)) finish(resolve);
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
} catch {
|
|
497
|
+
// Filesystem notifications are best-effort across platforms; the fallback
|
|
498
|
+
// timer keeps stale-lock/lease cleanup progressing if an event is dropped.
|
|
499
|
+
}
|
|
500
|
+
return promise;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function removeProviderInFlightLeaseDir(leasePath: string): Promise<void> {
|
|
504
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
505
|
+
try {
|
|
506
|
+
await fs.rm(leasePath, { recursive: true, force: true });
|
|
507
|
+
return;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (isEnoent(error)) return;
|
|
510
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
511
|
+
if (attempt < 2 && (code === "EBUSY" || code === "ENOTEMPTY" || code === "EPERM")) {
|
|
512
|
+
await Bun.sleep(25);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Signal into the lease's OWN provider directory (derived from `lease.path`)
|
|
521
|
+
// rather than recomputing it from the current root. A release that lands after
|
|
522
|
+
// the in-flight root has been repointed (only the test seam does that) must not
|
|
523
|
+
// write `.wakeup` into an unrelated provider directory.
|
|
524
|
+
async function releaseProviderInFlightLease(lease: ProviderInFlightLease): Promise<void> {
|
|
525
|
+
clearInterval(lease.heartbeat);
|
|
526
|
+
await lease.flushHeartbeat();
|
|
527
|
+
await removeProviderInFlightLeaseDir(lease.path);
|
|
528
|
+
await signalProviderInFlightWaitersInDir(path.dirname(lease.path));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function acquireProviderInFlightSlot(
|
|
532
|
+
provider: string,
|
|
533
|
+
limit: number | undefined,
|
|
534
|
+
signal?: AbortSignal,
|
|
535
|
+
): Promise<() => Promise<void>> {
|
|
536
|
+
if (limit === undefined) return async () => {};
|
|
537
|
+
let loggedWait = false;
|
|
538
|
+
while (true) {
|
|
539
|
+
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
540
|
+
const lease = await tryAcquireProviderInFlightLease(provider, limit, signal);
|
|
541
|
+
if (lease) return () => releaseProviderInFlightLease(lease);
|
|
542
|
+
if (!loggedWait) {
|
|
543
|
+
loggedWait = true;
|
|
544
|
+
logger.debug("Provider in-flight limit blocked request", { provider, limit });
|
|
545
|
+
}
|
|
546
|
+
await waitForProviderInFlightSignal(provider, signal);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export const __providerInFlightForTesting = {
|
|
551
|
+
setRoot(root: string | undefined): void {
|
|
552
|
+
providerInFlightRootOverride = root;
|
|
553
|
+
},
|
|
554
|
+
providerDir(provider: string): string {
|
|
555
|
+
return providerInFlightDir(provider);
|
|
556
|
+
},
|
|
557
|
+
lockDir(provider: string): string {
|
|
558
|
+
return providerInFlightLockDir(provider);
|
|
559
|
+
},
|
|
560
|
+
async captureStaleLockRelease(provider: string): Promise<(() => Promise<void>) | null> {
|
|
561
|
+
const lockDir = providerInFlightLockDir(provider);
|
|
562
|
+
const stale = await readProviderInFlightStaleLock(lockDir);
|
|
563
|
+
if (!stale) return null;
|
|
564
|
+
return () => releaseProviderInFlightStaleLock(lockDir, stale);
|
|
565
|
+
},
|
|
566
|
+
async captureLockDirRelease(provider: string): Promise<(() => Promise<void>) | null> {
|
|
567
|
+
const lockDir = providerInFlightLockDir(provider);
|
|
568
|
+
try {
|
|
569
|
+
const identity = await readProviderInFlightLockIdentity(lockDir);
|
|
570
|
+
return () => releaseProviderInFlightLockDirIfSame(lockDir, identity);
|
|
571
|
+
} catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
function withProviderInFlightLimit<TOptions extends Pick<StreamOptions, "signal" | "maxInFlightRequests">>(
|
|
578
|
+
model: Model<Api>,
|
|
579
|
+
options: TOptions | undefined,
|
|
580
|
+
dispatch: () => AssistantMessageEventStream,
|
|
581
|
+
): AssistantMessageEventStream {
|
|
582
|
+
// Leaked-thinking healing folds in here — the one shared provider-dispatch
|
|
583
|
+
// chokepoint — so the loop guard (which wraps this) sees healed events and all
|
|
584
|
+
// provider exits are covered by one wrap. Official first-party providers are
|
|
585
|
+
// exempt (see `healLeakedThinking`); healing is otherwise idempotent.
|
|
586
|
+
const limit = resolveProviderInFlightLimit(model.provider, options);
|
|
587
|
+
if (limit === undefined) return healLeakedThinking(model, dispatch());
|
|
588
|
+
|
|
589
|
+
const outer = new AssistantMessageEventStream();
|
|
590
|
+
void (async () => {
|
|
591
|
+
let release: (() => Promise<void>) | undefined;
|
|
592
|
+
let released = false;
|
|
593
|
+
const releaseOnce = async () => {
|
|
594
|
+
if (!release || released) return;
|
|
595
|
+
released = true;
|
|
596
|
+
await release();
|
|
597
|
+
};
|
|
598
|
+
try {
|
|
599
|
+
const startedWaitingAt = Date.now();
|
|
600
|
+
release = await acquireProviderInFlightSlot(model.provider, limit, options?.signal);
|
|
601
|
+
if (Date.now() - startedWaitingAt >= PROVIDER_INFLIGHT_SIGNAL_FALLBACK_MS) {
|
|
602
|
+
logger.debug("Provider in-flight limit wait completed", { provider: model.provider, limit });
|
|
603
|
+
}
|
|
604
|
+
if (options?.signal?.aborted) {
|
|
605
|
+
throw options.signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
606
|
+
}
|
|
607
|
+
const inner = healLeakedThinking(model, dispatch());
|
|
608
|
+
try {
|
|
609
|
+
for await (const event of inner) {
|
|
610
|
+
outer.push(event);
|
|
611
|
+
if (outer.done) return;
|
|
612
|
+
}
|
|
613
|
+
if (!outer.done) outer.end(await inner.result());
|
|
614
|
+
} finally {
|
|
615
|
+
await releaseOnce();
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
await releaseOnce();
|
|
619
|
+
if (!outer.done) outer.fail(error);
|
|
620
|
+
}
|
|
621
|
+
})();
|
|
622
|
+
return outer;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function createVertexAuthenticatedFetch(options: StreamOptions | undefined): FetchImpl {
|
|
626
|
+
const baseFetch = options?.fetch ?? fetch;
|
|
627
|
+
const vertexFetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
628
|
+
const token = await getVertexAccessToken({ signal: options?.signal, fetch: baseFetch });
|
|
629
|
+
const headers = new Headers(init?.headers);
|
|
630
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
631
|
+
const rewritten = resolveVertexRequest(input);
|
|
632
|
+
const url = rewritten instanceof Request ? rewritten.url : rewritten.toString();
|
|
633
|
+
if (isVertexRawPredictUrl(url)) {
|
|
634
|
+
const bodyText = await readVertexRequestBody(rewritten, init);
|
|
635
|
+
const transformed = transformVertexAnthropicBody(bodyText);
|
|
636
|
+
return baseFetch(url, {
|
|
637
|
+
...init,
|
|
638
|
+
method: init?.method ?? (rewritten instanceof Request ? rewritten.method : "POST"),
|
|
639
|
+
headers,
|
|
640
|
+
body: transformed,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return baseFetch(rewritten, { ...init, headers });
|
|
644
|
+
};
|
|
645
|
+
return Object.assign(vertexFetch, baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function readVertexRequestBody(input: string | URL | Request, init: RequestInit | undefined): Promise<string> {
|
|
649
|
+
if (input instanceof Request) return input.clone().text();
|
|
650
|
+
const body = init?.body;
|
|
651
|
+
if (typeof body === "string") return body;
|
|
652
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
|
|
653
|
+
if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
|
|
654
|
+
return "";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Vertex Claude rejects the standard Anthropic body shape: the `model` field
|
|
658
|
+
// is encoded in the URL path and `anthropic_version: "vertex-2023-10-16"` is
|
|
659
|
+
// required in the JSON body instead of the `anthropic-version` HTTP header.
|
|
660
|
+
function transformVertexAnthropicBody(bodyText: string): string {
|
|
661
|
+
if (!bodyText) return bodyText;
|
|
662
|
+
try {
|
|
663
|
+
const payload = JSON.parse(bodyText) as Record<string, unknown>;
|
|
664
|
+
delete payload.model;
|
|
665
|
+
payload.anthropic_version = "vertex-2023-10-16";
|
|
666
|
+
return JSON.stringify(payload);
|
|
667
|
+
} catch {
|
|
668
|
+
return bodyText;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function resolveVertexRequest(input: string | URL | Request): string | URL | Request {
|
|
673
|
+
const project = $env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT;
|
|
674
|
+
const location = $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
|
|
675
|
+
if (!project || !location) return input;
|
|
676
|
+
|
|
677
|
+
const rewriteUrl = (url: string): string => {
|
|
678
|
+
const hasPlaceholder =
|
|
679
|
+
url.includes("{project}") ||
|
|
680
|
+
url.includes("{location}") ||
|
|
681
|
+
url.includes("%7Bproject%7D") ||
|
|
682
|
+
url.includes("%7Blocation%7D");
|
|
683
|
+
const host = resolveVertexEndpointHost(location);
|
|
684
|
+
const rewritten = hasPlaceholder
|
|
685
|
+
? url
|
|
686
|
+
.replace("https://{location}-aiplatform.googleapis.com", `https://${host}`)
|
|
687
|
+
.replace("https://%7Blocation%7D-aiplatform.googleapis.com", `https://${host}`)
|
|
688
|
+
.replaceAll("{project}", encodeURIComponent(project))
|
|
689
|
+
.replaceAll("%7Bproject%7D", encodeURIComponent(project))
|
|
690
|
+
.replaceAll("{location}", encodeURIComponent(location))
|
|
691
|
+
.replaceAll("%7Blocation%7D", encodeURIComponent(location))
|
|
692
|
+
: url;
|
|
693
|
+
return rewritten.replace(":streamRawPredict/v1/messages", ":streamRawPredict");
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
if (input instanceof Request) {
|
|
697
|
+
const rewrittenUrl = rewriteUrl(input.url);
|
|
698
|
+
return rewrittenUrl === input.url ? input : new Request(rewrittenUrl, input);
|
|
699
|
+
}
|
|
700
|
+
if (input instanceof URL) {
|
|
701
|
+
const rewrittenUrl = rewriteUrl(input.toString());
|
|
702
|
+
return rewrittenUrl === input.toString() ? input : new URL(rewrittenUrl);
|
|
703
|
+
}
|
|
704
|
+
return rewriteUrl(input);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
type KeyResolver = string | (() => string | undefined);
|
|
708
|
+
|
|
709
|
+
const LEGACY_ENV_KEYS: Record<string, KeyResolver> = {
|
|
710
|
+
// Non-provider / search-tool keys and API-name keys not modeled as registry provider defs.
|
|
711
|
+
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
|
712
|
+
jina: "JINA_API_KEY",
|
|
713
|
+
brave: "BRAVE_API_KEY",
|
|
714
|
+
tinyfish: "TINYFISH_API_KEY",
|
|
715
|
+
firecrawl: "FIRECRAWL_API_KEY",
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Env fallbacks derived from the catalog table — the single source for plain
|
|
720
|
+
* provider env-var names. Registry defs override with computed resolvers
|
|
721
|
+
* (Foundry/ADC/Bedrock probes); legacy non-provider keys merge last.
|
|
722
|
+
*/
|
|
723
|
+
const CATALOG_ENTRY_ENV_KEYS = (CATALOG_PROVIDERS as readonly ProviderCatalogEntry[]).flatMap(provider => {
|
|
724
|
+
const envVars = provider.envVars;
|
|
725
|
+
if (!envVars || envVars.length === 0) return [];
|
|
726
|
+
const resolver: KeyResolver = envVars.length === 1 ? envVars[0] : () => $pickenv(...envVars);
|
|
727
|
+
return [[provider.id, resolver] as [string, KeyResolver]];
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const serviceProviderMap: Record<string, KeyResolver> = {
|
|
731
|
+
...Object.fromEntries(CATALOG_ENTRY_ENV_KEYS),
|
|
732
|
+
...Object.fromEntries(
|
|
733
|
+
PROVIDER_REGISTRY.flatMap(provider =>
|
|
734
|
+
provider.envKeys != null ? [[provider.id, provider.envKeys] as [string, KeyResolver]] : [],
|
|
735
|
+
),
|
|
736
|
+
),
|
|
737
|
+
...LEGACY_ENV_KEYS,
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
|
|
742
|
+
*
|
|
743
|
+
* Will not return API keys for providers that require OAuth tokens.
|
|
744
|
+
* Checks Bun.env, then cwd/.env, then ~/.env.
|
|
745
|
+
*/
|
|
746
|
+
export function getEnvApiKey(provider: string): string | undefined {
|
|
747
|
+
const resolver = serviceProviderMap[provider];
|
|
748
|
+
if (typeof resolver === "string") {
|
|
749
|
+
return $env[resolver];
|
|
750
|
+
}
|
|
751
|
+
return resolver?.();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Name of the environment variable that backs `getEnvApiKey` for a provider,
|
|
756
|
+
* when that provider maps to a single named variable (e.g. `github-copilot` →
|
|
757
|
+
* `COPILOT_GITHUB_TOKEN`). Returns undefined for providers whose env fallback
|
|
758
|
+
* is computed (multi-var pickers, Vertex ADC / Bedrock probes, …) since no
|
|
759
|
+
* single variable name describes the source.
|
|
760
|
+
*/
|
|
761
|
+
export function getEnvApiKeyName(provider: string): string | undefined {
|
|
762
|
+
const resolver = serviceProviderMap[provider];
|
|
763
|
+
return typeof resolver === "string" ? resolver : undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Enumerate every provider that has an env-var fallback for `getEnvApiKey`.
|
|
768
|
+
* Used by `omp auth-broker migrate --include-env` to discover env-sourced keys
|
|
769
|
+
* that should be uploaded to the broker.
|
|
770
|
+
*/
|
|
771
|
+
export function listProvidersWithEnvKey(): string[] {
|
|
772
|
+
return Object.keys(serviceProviderMap);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function stream<TApi extends Api>(
|
|
776
|
+
model: Model<TApi>,
|
|
777
|
+
context: Context,
|
|
778
|
+
options?: OptionsForApi<TApi>,
|
|
779
|
+
): AssistantMessageEventStream {
|
|
780
|
+
return withGeminiThinkingLoopGuard(model, options, opts =>
|
|
781
|
+
withProviderInFlightLimit(model, opts, () => streamDispatch(model, context, opts)),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function streamDispatch<TApi extends Api>(
|
|
786
|
+
model: Model<TApi>,
|
|
787
|
+
context: Context,
|
|
788
|
+
options?: OptionsForApi<TApi>,
|
|
789
|
+
): AssistantMessageEventStream {
|
|
790
|
+
const inputOptions = (options || {}) as StreamOptions;
|
|
791
|
+
const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
|
|
792
|
+
const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
|
|
793
|
+
const requestOptions = {
|
|
794
|
+
...debugOptions,
|
|
795
|
+
fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
|
|
796
|
+
} as OptionsForApi<TApi>;
|
|
797
|
+
assertExplicitOpenAIResponsesPromptCacheSupport(model, requestOptions);
|
|
798
|
+
|
|
799
|
+
// Check custom API registry first (extension-provided APIs like "vertex-claude-api")
|
|
800
|
+
const customApiProvider = getCustomApi(model.api);
|
|
801
|
+
if (customApiProvider) {
|
|
802
|
+
return customApiProvider.stream(model, context, requestOptions as StreamOptions);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (isGitLabDuoModel(model)) {
|
|
806
|
+
const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
|
|
807
|
+
if (!apiKey) {
|
|
808
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
809
|
+
}
|
|
810
|
+
return streamGitLabDuo(model, context, {
|
|
811
|
+
...(requestOptions as SimpleStreamOptions),
|
|
812
|
+
apiKey,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (model.api === "gitlab-duo-agent") {
|
|
817
|
+
const apiKey = (requestOptions as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
|
|
818
|
+
if (!apiKey) {
|
|
819
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
820
|
+
}
|
|
821
|
+
return streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
|
|
822
|
+
...(requestOptions as StreamOptions | undefined),
|
|
823
|
+
apiKey,
|
|
824
|
+
} as GitLabDuoWorkflowOptions);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Vertex AI and Bedrock Converse authenticate outside the generic API-key path.
|
|
828
|
+
if (model.api === "google-vertex") {
|
|
829
|
+
return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
|
|
830
|
+
}
|
|
831
|
+
if (model.api === "bedrock-converse-stream") {
|
|
832
|
+
return streamBedrock(model as Model<"bedrock-converse-stream">, context, requestOptions as BedrockOptions);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const prepareRequest = getProviderDefinition(model.provider)?.prepareRequest;
|
|
836
|
+
const prepared = prepareRequest?.(model as Model<Api>, requestOptions as StreamOptions);
|
|
837
|
+
const providerModel = prepared?.model ?? (model as Model<Api>);
|
|
838
|
+
const preparedOptions = prepared?.options ?? (requestOptions as StreamOptions);
|
|
839
|
+
const apiKey = preparedOptions.apiKey || getEnvApiKey(providerModel.provider);
|
|
840
|
+
if (!apiKey) {
|
|
841
|
+
throw new AIError.MissingApiKeyError(providerModel.provider);
|
|
842
|
+
}
|
|
843
|
+
const providerOptions = isGoogleVertexAuthenticatedModel(providerModel)
|
|
844
|
+
? {
|
|
845
|
+
...preparedOptions,
|
|
846
|
+
apiKey: "vertex-adc",
|
|
847
|
+
fetch: createVertexAuthenticatedFetch(preparedOptions),
|
|
848
|
+
}
|
|
849
|
+
: { ...preparedOptions, apiKey };
|
|
850
|
+
|
|
851
|
+
const api: Api = providerModel.api;
|
|
852
|
+
switch (api) {
|
|
853
|
+
case "anthropic-messages": {
|
|
854
|
+
const anthropicOptions = providerOptions as AnthropicOptions;
|
|
855
|
+
return streamAnthropic(providerModel as Model<"anthropic-messages">, context, {
|
|
856
|
+
...anthropicOptions,
|
|
857
|
+
isOAuth: anthropicOptions.isOAuth ?? providerModel.isOAuth,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
case "openrouter": {
|
|
862
|
+
const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
|
|
863
|
+
if (useResponses) {
|
|
864
|
+
return streamOpenAIResponses(
|
|
865
|
+
providerModel as Model<"openai-responses">,
|
|
866
|
+
context,
|
|
867
|
+
providerOptions as OptionsForApi<"openai-responses">,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return streamOpenAICompletions(
|
|
871
|
+
providerModel as Model<"openai-completions">,
|
|
872
|
+
context,
|
|
873
|
+
providerOptions as OptionsForApi<"openai-completions">,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
case "openai-completions":
|
|
878
|
+
return streamOpenAICompletions(
|
|
879
|
+
providerModel as Model<"openai-completions">,
|
|
880
|
+
context,
|
|
881
|
+
providerOptions as OptionsForApi<"openai-completions">,
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
case "openai-responses":
|
|
885
|
+
return streamOpenAIResponses(
|
|
886
|
+
providerModel as Model<"openai-responses">,
|
|
887
|
+
context,
|
|
888
|
+
providerOptions as OptionsForApi<"openai-responses">,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
case "azure-openai-responses":
|
|
892
|
+
return streamAzureOpenAIResponses(
|
|
893
|
+
providerModel as Model<"azure-openai-responses">,
|
|
894
|
+
context,
|
|
895
|
+
providerOptions as OptionsForApi<"azure-openai-responses">,
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
case "openai-codex-responses":
|
|
899
|
+
return streamOpenAICodexResponses(
|
|
900
|
+
providerModel as Model<"openai-codex-responses">,
|
|
901
|
+
context,
|
|
902
|
+
providerOptions as OptionsForApi<"openai-codex-responses">,
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
case "google-generative-ai":
|
|
906
|
+
return streamGoogle(providerModel as Model<"google-generative-ai">, context, providerOptions);
|
|
907
|
+
|
|
908
|
+
case "google-gemini-cli":
|
|
909
|
+
return streamGoogleGeminiCli(
|
|
910
|
+
providerModel as Model<"google-gemini-cli">,
|
|
911
|
+
context,
|
|
912
|
+
providerOptions as GoogleGeminiCliOptions,
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
case "ollama-chat":
|
|
916
|
+
return streamOllama(providerModel as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
|
|
917
|
+
|
|
918
|
+
case "cursor-agent":
|
|
919
|
+
return streamCursor(providerModel as Model<"cursor-agent">, context, providerOptions as CursorOptions);
|
|
920
|
+
|
|
921
|
+
case "devin-agent":
|
|
922
|
+
return streamDevin(providerModel as Model<"devin-agent">, context, providerOptions as DevinOptions);
|
|
923
|
+
|
|
924
|
+
default:
|
|
925
|
+
throw new AIError.ConfigurationError(`Unhandled API: ${api}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Thinking-loop re-samples spent before {@link resolveWithThinkingLoopCook} cooks. */
|
|
930
|
+
const THINKING_LOOP_MAX_ABORTS = 3;
|
|
931
|
+
const THINKING_LOOP_RETRY_BASE_DELAY_MS = 500;
|
|
932
|
+
const THINKING_LOOP_RETRY_MAX_DELAY_MS = 8_000;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Resolve a completion, re-sampling a thinking-loop stall up to
|
|
936
|
+
* {@link THINKING_LOOP_MAX_ABORTS} times before letting it cook. The loop guard
|
|
937
|
+
* raises an empty `stopReason: "error"` stall on each guarded attempt; this
|
|
938
|
+
* result-path consumer re-dispatches a fresh request per stall and, once the abort
|
|
939
|
+
* budget is spent, runs one final pass with the guard disabled so a stubborn loop
|
|
940
|
+
* returns the model's raw output instead of a fatal stall. Non-stall results —
|
|
941
|
+
* including genuine errors — return immediately; a caller abort during backoff
|
|
942
|
+
* propagates so cancellation surfaces as an abort, never a stale stall result.
|
|
943
|
+
*/
|
|
944
|
+
async function resolveWithThinkingLoopCook(
|
|
945
|
+
signal: AbortSignal | undefined,
|
|
946
|
+
dispatch: () => AssistantMessageEventStream,
|
|
947
|
+
cook: () => AssistantMessageEventStream,
|
|
948
|
+
): Promise<AssistantMessage> {
|
|
949
|
+
let message = await dispatch().result();
|
|
950
|
+
let thinkingLoopRetry = AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
|
|
951
|
+
for (let attempt = 0; thinkingLoopRetry && attempt < THINKING_LOOP_MAX_ABORTS - 1; attempt += 1) {
|
|
952
|
+
// A caller abort surfaces as a thrown abort (never the stall, which would
|
|
953
|
+
// misclassify as a 502): throwIfAborted before backoff, and scheduler.wait
|
|
954
|
+
// rejects if the abort lands mid-delay.
|
|
955
|
+
signal?.throwIfAborted();
|
|
956
|
+
const delay = Math.min(THINKING_LOOP_RETRY_BASE_DELAY_MS * 2 ** attempt, THINKING_LOOP_RETRY_MAX_DELAY_MS);
|
|
957
|
+
await scheduler.wait(delay, { signal });
|
|
958
|
+
message = await dispatch().result();
|
|
959
|
+
thinkingLoopRetry =
|
|
960
|
+
message.stopReason === "error" &&
|
|
961
|
+
message.content.length === 0 &&
|
|
962
|
+
AIError.is(message.errorId, AIError.Flag.ThinkingLoop);
|
|
963
|
+
}
|
|
964
|
+
if (!thinkingLoopRetry) return message;
|
|
965
|
+
signal?.throwIfAborted();
|
|
966
|
+
// Abort budget spent and still looping: let it cook with the guard disabled.
|
|
967
|
+
return cook().result();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export async function complete<TApi extends Api>(
|
|
971
|
+
model: Model<TApi>,
|
|
972
|
+
context: Context,
|
|
973
|
+
options?: OptionsForApi<TApi>,
|
|
974
|
+
): Promise<AssistantMessage> {
|
|
975
|
+
return resolveWithThinkingLoopCook(
|
|
976
|
+
options?.signal,
|
|
977
|
+
() => stream(model, context, options),
|
|
978
|
+
() => stream(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
type AuthRetryFailure = {
|
|
983
|
+
error: unknown;
|
|
984
|
+
bufferedEvents: AssistantMessageEvent[];
|
|
985
|
+
terminalEvent?: Extract<AssistantMessageEvent, { type: "error" }>;
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
function extractStatusFromAssistantError(message: AssistantMessage): number | undefined {
|
|
989
|
+
if (message.errorStatus !== undefined) return message.errorStatus;
|
|
990
|
+
if (!message.errorMessage) return undefined;
|
|
991
|
+
return AIError.status({ message: message.errorMessage });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isRetryableUpstreamError(error: unknown, status: number | undefined, message: string | undefined): boolean {
|
|
995
|
+
// 401 means the credential is bad; 403 is its valid-token twin (access
|
|
996
|
+
// denied by plan, model policy, or org restriction — a sibling account may
|
|
997
|
+
// not share it). Usage-limit phrasing (Codex's
|
|
998
|
+
// "You have hit your ChatGPT usage limit", Anthropic's "usage_limit_reached",
|
|
999
|
+
// Google's "resource_exhausted", OpenAI's "insufficient_quota") and 429s
|
|
1000
|
+
// without transient rate-limit wording mean this account is parked but a
|
|
1001
|
+
// sibling credential can usually pick the request up. Both are rotatable
|
|
1002
|
+
// via `onAuthError` — the auth-gateway maps the former to
|
|
1003
|
+
// `invalidateCredentialMatching` and the latter to
|
|
1004
|
+
// `markUsageLimitReached`. Transient 429s ("Too many requests",
|
|
1005
|
+
// per-minute caps) classify as RATE_LIMIT_EXCEEDED in
|
|
1006
|
+
// `parseRateLimitReason` and stay in the provider's own backoff layer
|
|
1007
|
+
// instead of burning siblings.
|
|
1008
|
+
if (AIError.isUsageLimit(error)) return true;
|
|
1009
|
+
if (isInvalidatedOAuthTokenError(error)) return true;
|
|
1010
|
+
if (status === 401 || (status === 403 && !isConcurrencyCapExclusion(status, message))) return true;
|
|
1011
|
+
return isUsageLimitOutcome(status, message);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function createAssistantAuthError(message: AssistantMessage): Error {
|
|
1015
|
+
const text = message.errorMessage ?? "Provider authentication failed";
|
|
1016
|
+
const status = extractStatusFromAssistantError(message);
|
|
1017
|
+
const error =
|
|
1018
|
+
status === undefined
|
|
1019
|
+
? new AIError.ProviderResponseError(text, { kind: "runtime" })
|
|
1020
|
+
: new ProviderHttpError(text, status);
|
|
1021
|
+
return typeof message.errorId === "number" ? AIError.attach(error, message.errorId) : error;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function emitBufferedEvents(stream: AssistantMessageEventStream, events: AssistantMessageEvent[]): void {
|
|
1025
|
+
for (const event of events) {
|
|
1026
|
+
stream.push(event);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export function streamSimple<TApi extends Api>(
|
|
1031
|
+
model: Model<TApi>,
|
|
1032
|
+
context: Context,
|
|
1033
|
+
options?: SimpleStreamOptions,
|
|
1034
|
+
): AssistantMessageEventStream {
|
|
1035
|
+
const inputOptions = (options || {}) as SimpleStreamOptions;
|
|
1036
|
+
const baseOptions = { ...inputOptions, fetch: inputOptions.fetch ?? defaultFetchForModel(model) };
|
|
1037
|
+
const debugOptions = withExtraCaFetch(withRequestDebugFetch(baseOptions));
|
|
1038
|
+
const requestOptions = {
|
|
1039
|
+
...debugOptions,
|
|
1040
|
+
fetch: wrapFetchForProxy(debugOptions.fetch, model.provider),
|
|
1041
|
+
} as SimpleStreamOptions;
|
|
1042
|
+
|
|
1043
|
+
const apiKeyResolver = isApiKeyResolver(requestOptions?.apiKey) ? requestOptions.apiKey : undefined;
|
|
1044
|
+
if (apiKeyResolver) {
|
|
1045
|
+
const outer = new AssistantMessageEventStream();
|
|
1046
|
+
const signal = requestOptions?.signal;
|
|
1047
|
+
// One inner attempt against a resolved key, or against the Bedrock AWS
|
|
1048
|
+
// credential chain when its optional resolver has no stored bearer key.
|
|
1049
|
+
// Retryable auth failures are buffered until replay is safe.
|
|
1050
|
+
const runAttempt = async (apiKey?: string): Promise<AuthRetryFailure | undefined> => {
|
|
1051
|
+
const bufferedEvents: AssistantMessageEvent[] = [];
|
|
1052
|
+
let emittedReplayUnsafeEvent = false;
|
|
1053
|
+
const flushBuffered = (): void => {
|
|
1054
|
+
emitBufferedEvents(outer, bufferedEvents);
|
|
1055
|
+
bufferedEvents.length = 0;
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const inner = streamSimple(model, context, { ...requestOptions, apiKey });
|
|
1060
|
+
for await (const event of inner) {
|
|
1061
|
+
if (!emittedReplayUnsafeEvent && event.type === "start") {
|
|
1062
|
+
bufferedEvents.push(event);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (
|
|
1066
|
+
!emittedReplayUnsafeEvent &&
|
|
1067
|
+
event.type === "error" &&
|
|
1068
|
+
isRetryableUpstreamError(
|
|
1069
|
+
event.error,
|
|
1070
|
+
extractStatusFromAssistantError(event.error),
|
|
1071
|
+
event.error.errorMessage,
|
|
1072
|
+
)
|
|
1073
|
+
) {
|
|
1074
|
+
return { error: createAssistantAuthError(event.error), bufferedEvents, terminalEvent: event };
|
|
1075
|
+
}
|
|
1076
|
+
flushBuffered();
|
|
1077
|
+
emittedReplayUnsafeEvent = true;
|
|
1078
|
+
outer.push(event);
|
|
1079
|
+
if (outer.done) return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
flushBuffered();
|
|
1082
|
+
if (!outer.done) outer.end(await inner.result());
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (
|
|
1085
|
+
!emittedReplayUnsafeEvent &&
|
|
1086
|
+
isRetryableUpstreamError(
|
|
1087
|
+
error,
|
|
1088
|
+
AIError.status(error),
|
|
1089
|
+
error instanceof Error ? error.message : undefined,
|
|
1090
|
+
)
|
|
1091
|
+
) {
|
|
1092
|
+
return { error, bufferedEvents };
|
|
1093
|
+
}
|
|
1094
|
+
flushBuffered();
|
|
1095
|
+
outer.fail(error);
|
|
1096
|
+
}
|
|
1097
|
+
return undefined;
|
|
1098
|
+
};
|
|
1099
|
+
const emitFailure = (failure: AuthRetryFailure): void => {
|
|
1100
|
+
emitBufferedEvents(outer, failure.bufferedEvents);
|
|
1101
|
+
if (failure.terminalEvent) {
|
|
1102
|
+
outer.push(failure.terminalEvent);
|
|
1103
|
+
} else {
|
|
1104
|
+
outer.fail(failure.error);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
void (async () => {
|
|
1109
|
+
let lastKey: string | undefined;
|
|
1110
|
+
try {
|
|
1111
|
+
lastKey = (await apiKeyResolver({ lastChance: false, error: undefined, signal })) || undefined;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
// A thrown resolver is a broker/OAuth/network failure, not a missing
|
|
1114
|
+
// key — surface the cause instead of masking it as "No API key".
|
|
1115
|
+
outer.fail(
|
|
1116
|
+
new AIError.ConfigurationError(
|
|
1117
|
+
`Failed to resolve API key for provider ${model.provider}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1118
|
+
{ cause: error },
|
|
1119
|
+
),
|
|
1120
|
+
);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (lastKey === undefined) {
|
|
1124
|
+
if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1125
|
+
const failure = await runAttempt();
|
|
1126
|
+
if (failure) emitFailure(failure);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
outer.fail(new AIError.MissingApiKeyError(model.provider));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const retryState = createAuthRetryKeyState(lastKey);
|
|
1133
|
+
let failure = await runAttempt(lastKey);
|
|
1134
|
+
if (!failure) return;
|
|
1135
|
+
while (true) {
|
|
1136
|
+
// Caller aborted between attempts: don't mint a fresh token or fire
|
|
1137
|
+
// another doomed request — emit the captured failure instead.
|
|
1138
|
+
if (signal?.aborted) break;
|
|
1139
|
+
const nextKey = await resolveNextAuthRetryKey(retryState, apiKeyResolver, failure.error, signal);
|
|
1140
|
+
if (nextKey === undefined) break;
|
|
1141
|
+
const next = await runAttempt(nextKey);
|
|
1142
|
+
if (!next) return;
|
|
1143
|
+
failure = next;
|
|
1144
|
+
}
|
|
1145
|
+
emitFailure(failure);
|
|
1146
|
+
})();
|
|
1147
|
+
return outer;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Pi-native transport short-circuits the per-provider dispatch entirely:
|
|
1151
|
+
// the gateway resolves provider + credential server-side, so we don't
|
|
1152
|
+
// need an `apiKey` from `getEnvApiKey` here — `options.apiKey` carries
|
|
1153
|
+
// the gateway bearer instead. Comes BEFORE the custom-API check so
|
|
1154
|
+
// extension-registered APIs can't accidentally override a configured
|
|
1155
|
+
// pi-native transport.
|
|
1156
|
+
if (model.transport === "pi-native") {
|
|
1157
|
+
return withGeminiThinkingLoopGuard(model, requestOptions, opts =>
|
|
1158
|
+
withProviderInFlightLimit(model, opts, () => streamPiNative(model, context, opts)),
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Check custom API registry (extension-provided APIs)
|
|
1163
|
+
const customApiProvider = getCustomApi(model.api);
|
|
1164
|
+
if (customApiProvider) {
|
|
1165
|
+
return withGeminiThinkingLoopGuard(model, requestOptions, opts =>
|
|
1166
|
+
withProviderInFlightLimit(model, opts, () => customApiProvider.streamSimple(model, context, opts)),
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Vertex AI uses Application Default Credentials, not API keys
|
|
1171
|
+
if (model.api === "google-vertex") {
|
|
1172
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
1173
|
+
return stream(model, context, providerOptions);
|
|
1174
|
+
} else if (model.api === "bedrock-converse-stream") {
|
|
1175
|
+
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
1176
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
1177
|
+
return stream(model, context, providerOptions);
|
|
1178
|
+
} else if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1179
|
+
const providerOptions = mapOptionsForApi(
|
|
1180
|
+
model,
|
|
1181
|
+
requestOptions,
|
|
1182
|
+
typeof requestOptions.apiKey === "string" ? requestOptions.apiKey : getEnvApiKey(model.provider),
|
|
1183
|
+
);
|
|
1184
|
+
return stream(model, context, providerOptions);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// The resolver form is handled by the wrapper above; only a static string
|
|
1188
|
+
// key reaches this point.
|
|
1189
|
+
const apiKey =
|
|
1190
|
+
(typeof requestOptions?.apiKey === "string" ? requestOptions.apiKey : undefined) || getEnvApiKey(model.provider);
|
|
1191
|
+
if (!apiKey) {
|
|
1192
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
|
1196
|
+
if (isGitLabDuoModel(model)) {
|
|
1197
|
+
return withProviderInFlightLimit(model, requestOptions, () =>
|
|
1198
|
+
streamGitLabDuo(model, context, {
|
|
1199
|
+
...requestOptions,
|
|
1200
|
+
apiKey,
|
|
1201
|
+
}),
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// GitLab Duo Workflow - IDE workflow protocol + WebSocket action bridge
|
|
1206
|
+
if (model.api === "gitlab-duo-agent") {
|
|
1207
|
+
// Does not route through withProviderInFlightLimit, so heal explicitly.
|
|
1208
|
+
return healLeakedThinking(
|
|
1209
|
+
model,
|
|
1210
|
+
streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
|
|
1211
|
+
...requestOptions,
|
|
1212
|
+
apiKey,
|
|
1213
|
+
}),
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
1218
|
+
if (isKimiModel(model)) {
|
|
1219
|
+
// streamKimi handles openai/anthropic format mapping internally, but the
|
|
1220
|
+
// mandatory-reasoning clamp is a request-shaping concern owned here: K3's
|
|
1221
|
+
// `supports_thinking_type: "only"` endpoint rejects disabled/omitted
|
|
1222
|
+
// thinking, so clamp disabled requests to the lowest supported effort
|
|
1223
|
+
// (mirrors the mapOptionsForApi path every other provider takes).
|
|
1224
|
+
const kimiOptions = normalizeMandatoryReasoningOptions(model, requestOptions);
|
|
1225
|
+
return withProviderInFlightLimit(model, kimiOptions, () =>
|
|
1226
|
+
streamKimi(model as Model<"openai-completions">, context, {
|
|
1227
|
+
...kimiOptions,
|
|
1228
|
+
apiKey,
|
|
1229
|
+
format: kimiOptions?.kimiApiFormat,
|
|
1230
|
+
}),
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Synthetic - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
1235
|
+
if (isSyntheticModel(model)) {
|
|
1236
|
+
// Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
|
|
1237
|
+
return withProviderInFlightLimit(model, requestOptions, () =>
|
|
1238
|
+
streamSynthetic(model as Model<"openai-completions">, context, {
|
|
1239
|
+
...requestOptions,
|
|
1240
|
+
apiKey,
|
|
1241
|
+
format: requestOptions?.syntheticApiFormat ?? "openai", // Default to OpenAI format
|
|
1242
|
+
}),
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, apiKey);
|
|
1246
|
+
return stream(model, context, providerOptions);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
export async function completeSimple<TApi extends Api>(
|
|
1250
|
+
model: Model<TApi>,
|
|
1251
|
+
context: Context,
|
|
1252
|
+
options?: SimpleStreamOptions,
|
|
1253
|
+
): Promise<AssistantMessage> {
|
|
1254
|
+
return resolveWithThinkingLoopCook(
|
|
1255
|
+
options?.signal,
|
|
1256
|
+
() => streamSimple(model, context, options),
|
|
1257
|
+
() => streamSimple(model, context, { ...options, loopGuard: { ...options?.loopGuard, enabled: false } }),
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const MIN_OUTPUT_TOKENS = 1024;
|
|
1262
|
+
// Fallback total output cap for models whose catalog entry has no maxTokens.
|
|
1263
|
+
const OUTPUT_CAP_WHEN_UNKNOWN = 64_000;
|
|
1264
|
+
function maxTokensWithThinkingBudget(
|
|
1265
|
+
baseMaxTokens: number | undefined,
|
|
1266
|
+
modelMaxTokens: number | null,
|
|
1267
|
+
thinkingBudget: number,
|
|
1268
|
+
): number {
|
|
1269
|
+
const uncappedMaxTokens = baseMaxTokens === undefined ? OUTPUT_CAP_WHEN_UNKNOWN : baseMaxTokens + thinkingBudget;
|
|
1270
|
+
return Math.min(uncappedMaxTokens, modelMaxTokens ?? Number.POSITIVE_INFINITY);
|
|
1271
|
+
}
|
|
1272
|
+
export const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
1273
|
+
const ANTHROPIC_USE_INTERLEAVED_THINKING = Bun.env.PI_NO_INTERLEAVED_THINKING !== "1";
|
|
1274
|
+
|
|
1275
|
+
export const ANTHROPIC_THINKING: Record<Effort, number> = {
|
|
1276
|
+
minimal: 1024,
|
|
1277
|
+
low: 4096,
|
|
1278
|
+
medium: 8192,
|
|
1279
|
+
high: 16384,
|
|
1280
|
+
xhigh: 32768,
|
|
1281
|
+
max: 32768,
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const GOOGLE_THINKING: Record<Effort, number> = {
|
|
1285
|
+
minimal: 1024,
|
|
1286
|
+
low: 4096,
|
|
1287
|
+
medium: 8192,
|
|
1288
|
+
high: 16384,
|
|
1289
|
+
xhigh: 24575,
|
|
1290
|
+
max: 32768,
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const BEDROCK_CLAUDE_THINKING: Record<Effort, number> = {
|
|
1294
|
+
minimal: 1024,
|
|
1295
|
+
low: 2048,
|
|
1296
|
+
medium: 8192,
|
|
1297
|
+
high: 16384,
|
|
1298
|
+
xhigh: 16384,
|
|
1299
|
+
max: 32768,
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
function resolveBedrockThinkingBudget(
|
|
1303
|
+
model: Model<"bedrock-converse-stream">,
|
|
1304
|
+
options?: SimpleStreamOptions,
|
|
1305
|
+
): { budget: number; level: Effort } | null {
|
|
1306
|
+
if (!options?.reasoning || !model.reasoning) return null;
|
|
1307
|
+
const level = requireSupportedEffort(model, options.reasoning);
|
|
1308
|
+
const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
|
|
1309
|
+
return { budget, level };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"] {
|
|
1313
|
+
if (!choice) return undefined;
|
|
1314
|
+
if (typeof choice === "string") {
|
|
1315
|
+
if (choice === "required") return "any";
|
|
1316
|
+
if (choice === "auto" || choice === "none" || choice === "any") return choice;
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
if (choice.type === "tool") {
|
|
1320
|
+
return choice.name ? { type: "tool", name: choice.name } : undefined;
|
|
1321
|
+
}
|
|
1322
|
+
if (choice.type === "function") {
|
|
1323
|
+
const name = "function" in choice ? choice.function?.name : choice.name;
|
|
1324
|
+
return name ? { type: "tool", name } : undefined;
|
|
1325
|
+
}
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
export function mapGoogleToolChoice(
|
|
1330
|
+
choice?: ToolChoice,
|
|
1331
|
+
): GoogleOptions["toolChoice"] | GoogleGeminiCliOptions["toolChoice"] | GoogleVertexOptions["toolChoice"] {
|
|
1332
|
+
if (!choice) return undefined;
|
|
1333
|
+
if (typeof choice === "string") {
|
|
1334
|
+
if (choice === "required") return "any";
|
|
1335
|
+
if (choice === "auto" || choice === "none" || choice === "any") return choice;
|
|
1336
|
+
return undefined;
|
|
1337
|
+
}
|
|
1338
|
+
// Named-tool routing on Google: emit an `ANY`-mode allow-list of one entry,
|
|
1339
|
+
// mirroring the Anthropic mapper that returns `{type: "tool", name}`.
|
|
1340
|
+
if (choice.type === "tool") {
|
|
1341
|
+
return choice.name ? { mode: "ANY", allowedFunctionNames: [choice.name] } : undefined;
|
|
1342
|
+
}
|
|
1343
|
+
if (choice.type === "function") {
|
|
1344
|
+
const name = "function" in choice ? choice.function?.name : choice.name;
|
|
1345
|
+
return name ? { mode: "ANY", allowedFunctionNames: [name] } : undefined;
|
|
1346
|
+
}
|
|
1347
|
+
return undefined;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function mapOpenAiToolChoice(choice?: ToolChoice): OpenAICompletionsOptions["toolChoice"] {
|
|
1351
|
+
if (!choice) return undefined;
|
|
1352
|
+
if (typeof choice === "string") {
|
|
1353
|
+
if (choice === "any") return "required";
|
|
1354
|
+
if (choice === "auto" || choice === "none" || choice === "required") return choice;
|
|
1355
|
+
return undefined;
|
|
1356
|
+
}
|
|
1357
|
+
if (choice.type === "tool") {
|
|
1358
|
+
return choice.name ? { type: "function", function: { name: choice.name } } : undefined;
|
|
1359
|
+
}
|
|
1360
|
+
if (choice.type === "function") {
|
|
1361
|
+
const name = "function" in choice ? choice.function?.name : choice.name;
|
|
1362
|
+
return name ? { type: "function", function: { name } } : undefined;
|
|
1363
|
+
}
|
|
1364
|
+
return undefined;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
type ReasoningEffortMapCompat = {
|
|
1368
|
+
reasoningEffortMap?: Partial<Record<Effort, string>>;
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
function getCompatReasoningEffortMap<TApi extends Api>(
|
|
1372
|
+
model: Model<TApi>,
|
|
1373
|
+
): Partial<Record<Effort, string>> | undefined {
|
|
1374
|
+
const compat = model.compat;
|
|
1375
|
+
if (compat === undefined || typeof compat !== "object" || !("reasoningEffortMap" in compat)) {
|
|
1376
|
+
return undefined;
|
|
1377
|
+
}
|
|
1378
|
+
return (compat as ReasoningEffortMapCompat).reasoningEffortMap;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function resolveSupportedMappedReasoningEffort<TApi extends Api>(
|
|
1382
|
+
model: Model<TApi>,
|
|
1383
|
+
reasoning: Effort,
|
|
1384
|
+
): Effort | undefined {
|
|
1385
|
+
const mapped = getCompatReasoningEffortMap(model)?.[reasoning];
|
|
1386
|
+
if (!mapped) return undefined;
|
|
1387
|
+
const mappedEffort = mapped as Effort;
|
|
1388
|
+
return model.thinking?.efforts.includes(mappedEffort) ? mappedEffort : undefined;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
1392
|
+
model: Model<TApi>,
|
|
1393
|
+
options?: SimpleStreamOptions,
|
|
1394
|
+
): Effort | undefined {
|
|
1395
|
+
const reasoning = options?.reasoning;
|
|
1396
|
+
if (!reasoning || !model.reasoning) return undefined;
|
|
1397
|
+
// Models that reason natively but expose no effort dial carry
|
|
1398
|
+
// `thinking: undefined` (baked at build time from
|
|
1399
|
+
// `compat.supportsReasoningEffort: false` on openai-responses*). The
|
|
1400
|
+
// wire-side omitReasoningEffort gate (stream.ts) is the actual strip; returning
|
|
1401
|
+
// undefined here avoids a redundant requireSupportedEffort throw that would
|
|
1402
|
+
// defeat the gate and surface a confusing "Compaction failed: Thinking effort
|
|
1403
|
+
// high is not supported by..." to the user.
|
|
1404
|
+
if (!model.thinking) return undefined;
|
|
1405
|
+
if (model.thinking.efforts.includes(reasoning)) return reasoning;
|
|
1406
|
+
const mappedReasoning = resolveSupportedMappedReasoningEffort(model, reasoning);
|
|
1407
|
+
if (mappedReasoning) return mappedReasoning;
|
|
1408
|
+
if (getCompatReasoningEffortMap(model)?.[reasoning] !== undefined) return reasoning;
|
|
1409
|
+
if (model.thinking.effortMap?.[reasoning] !== undefined) return reasoning;
|
|
1410
|
+
return requireSupportedEffort(model, reasoning);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Mandatory-reasoning endpoints (`thinking.requiresEffort`) reject disabled
|
|
1417
|
+
* or omitted thinking ("Reasoning is mandatory for this endpoint and cannot
|
|
1418
|
+
* be disabled") — clamp to the lowest supported effort instead.
|
|
1419
|
+
* `suppressWhenOff` models handle off provider-side via explicit wire
|
|
1420
|
+
* suppression. Collapsed pairs interplay: pair derivation strips member
|
|
1421
|
+
* flags (off routes to a bare SKU that CAN disable), while identity backfill
|
|
1422
|
+
* re-flags pairs whose logical id is itself mandatory (Gemini 3.x) — there
|
|
1423
|
+
* the clamp wins and the floored effort routes to the thinking SKU.
|
|
1424
|
+
*/
|
|
1425
|
+
function normalizeMandatoryReasoningOptions<TApi extends Api>(
|
|
1426
|
+
model: Model<TApi>,
|
|
1427
|
+
options?: SimpleStreamOptions,
|
|
1428
|
+
): SimpleStreamOptions | undefined {
|
|
1429
|
+
if (
|
|
1430
|
+
!model.reasoning ||
|
|
1431
|
+
!model.thinking?.requiresEffort ||
|
|
1432
|
+
model.thinking.suppressWhenOff ||
|
|
1433
|
+
(options?.reasoning !== undefined && !options.disableReasoning)
|
|
1434
|
+
) {
|
|
1435
|
+
return options;
|
|
1436
|
+
}
|
|
1437
|
+
const floor = minimumSupportedEffort(model);
|
|
1438
|
+
if (floor === undefined) return options;
|
|
1439
|
+
return { ...options, reasoning: floor, disableReasoning: undefined };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function supportsExplicitOpenAIResponsesPromptCache(compat: unknown): boolean {
|
|
1443
|
+
return (
|
|
1444
|
+
typeof compat === "object" &&
|
|
1445
|
+
compat !== null &&
|
|
1446
|
+
"supportsPromptCacheBreakpoints" in compat &&
|
|
1447
|
+
compat.supportsPromptCacheBreakpoints === true
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function isOpenAIResponsesPromptCacheSurface<TApi extends Api>(model: Model<TApi>): boolean {
|
|
1452
|
+
return (
|
|
1453
|
+
model.api === "openai-responses" ||
|
|
1454
|
+
model.api === "azure-openai-responses" ||
|
|
1455
|
+
(model.api === "openrouter" && $env.PI_OPENROUTER_RESPONSES !== "0")
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function assertExplicitOpenAIResponsesPromptCacheSupport<TApi extends Api>(
|
|
1460
|
+
model: Model<TApi>,
|
|
1461
|
+
options?: StreamOptions,
|
|
1462
|
+
): void {
|
|
1463
|
+
if (
|
|
1464
|
+
model.transport === "pi-native" ||
|
|
1465
|
+
resolveCacheRetention(options?.cacheRetention) === "none" ||
|
|
1466
|
+
options?.promptCache?.mode !== "explicit" ||
|
|
1467
|
+
!isOpenAIResponsesPromptCacheSurface(model) ||
|
|
1468
|
+
supportsExplicitOpenAIResponsesPromptCache(model.compat)
|
|
1469
|
+
) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
throw new AIError.ConfigurationError(
|
|
1473
|
+
`OpenAI explicit prompt caching is unsupported for ${model.provider}/${model.id}; enable compat.supportsPromptCacheBreakpoints only for a compatible endpoint.`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function mapOptionsForApi<TApi extends Api>(
|
|
1478
|
+
model: Model<TApi>,
|
|
1479
|
+
rawOptions?: SimpleStreamOptions,
|
|
1480
|
+
apiKey?: string,
|
|
1481
|
+
): OptionsForApi<TApi> {
|
|
1482
|
+
const options = normalizeMandatoryReasoningOptions(model, rawOptions);
|
|
1483
|
+
const simpleProviderOptions = getProviderDefinition(model.provider)?.mapSimpleOptions?.(options ?? {});
|
|
1484
|
+
const base = {
|
|
1485
|
+
temperature: options?.temperature,
|
|
1486
|
+
topP: options?.topP,
|
|
1487
|
+
topK: options?.topK,
|
|
1488
|
+
minP: options?.minP,
|
|
1489
|
+
presencePenalty: options?.presencePenalty,
|
|
1490
|
+
repetitionPenalty: options?.repetitionPenalty,
|
|
1491
|
+
maxTokens: options?.maxTokens ?? model.maxTokens ?? undefined,
|
|
1492
|
+
signal: options?.signal,
|
|
1493
|
+
apiKey: apiKey ?? (typeof options?.apiKey === "string" ? options.apiKey : undefined),
|
|
1494
|
+
cacheRetention: options?.cacheRetention,
|
|
1495
|
+
headers: options?.headers,
|
|
1496
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1497
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1498
|
+
metadata: options?.metadata,
|
|
1499
|
+
taskBudget: options?.taskBudget,
|
|
1500
|
+
sessionId: options?.sessionId,
|
|
1501
|
+
promptCacheKey: options?.promptCacheKey,
|
|
1502
|
+
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
1503
|
+
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
1504
|
+
codexSseMaxAttempts: options?.codexSseMaxAttempts,
|
|
1505
|
+
providerSessionState: options?.providerSessionState,
|
|
1506
|
+
maxInFlightRequests: options?.maxInFlightRequests,
|
|
1507
|
+
onPayload: options?.onPayload,
|
|
1508
|
+
onResponse: options?.onResponse,
|
|
1509
|
+
onSseEvent: options?.onSseEvent,
|
|
1510
|
+
execHandlers: options?.execHandlers,
|
|
1511
|
+
fetch: options?.fetch,
|
|
1512
|
+
fallbacks: options?.fallbacks,
|
|
1513
|
+
...simpleProviderOptions,
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
switch (model.api) {
|
|
1517
|
+
case "anthropic-messages": {
|
|
1518
|
+
// Explicitly disable thinking when reasoning is not specified, the caller
|
|
1519
|
+
// disabled it, or the model doesn't support it. `disableReasoning` is a
|
|
1520
|
+
// SimpleStreamOptions flag that never reaches AnthropicOptions on its own,
|
|
1521
|
+
// so it must be folded into `thinkingEnabled` here (mandatory-reasoning
|
|
1522
|
+
// models already clamp it away in normalizeMandatoryReasoningOptions).
|
|
1523
|
+
const reasoning = options?.reasoning;
|
|
1524
|
+
if (!reasoning || !model.reasoning || options?.disableReasoning) {
|
|
1525
|
+
return castApi<"anthropic-messages">({
|
|
1526
|
+
...base,
|
|
1527
|
+
requestModelId: resolveWireModelId(model, undefined),
|
|
1528
|
+
thinkingEnabled: false,
|
|
1529
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1530
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1531
|
+
serviceTier: options?.serviceTier,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
let thinkingBudget = options.thinkingBudgets?.[reasoning] ?? ANTHROPIC_THINKING[reasoning];
|
|
1536
|
+
if (thinkingBudget <= 0) {
|
|
1537
|
+
return castApi<"anthropic-messages">({
|
|
1538
|
+
...base,
|
|
1539
|
+
requestModelId: resolveWireModelId(model, undefined),
|
|
1540
|
+
thinkingEnabled: false,
|
|
1541
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1542
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1543
|
+
serviceTier: options?.serviceTier,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const thinkingMode = model.thinking?.mode;
|
|
1548
|
+
const effort =
|
|
1549
|
+
thinkingMode === "anthropic-adaptive" || thinkingMode === "anthropic-budget-effort"
|
|
1550
|
+
? mapEffortToAnthropicAdaptiveEffort(model, reasoning)
|
|
1551
|
+
: undefined;
|
|
1552
|
+
|
|
1553
|
+
// For Opus 4.6+ and Sonnet 4.6+: use adaptive thinking with effort level
|
|
1554
|
+
// For older models: use budget-based thinking
|
|
1555
|
+
if (thinkingMode === "anthropic-adaptive") {
|
|
1556
|
+
return castApi<"anthropic-messages">({
|
|
1557
|
+
...base,
|
|
1558
|
+
requestModelId: resolveWireModelId(model, reasoning),
|
|
1559
|
+
thinkingEnabled: true,
|
|
1560
|
+
effort,
|
|
1561
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1562
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1563
|
+
serviceTier: options?.serviceTier,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (ANTHROPIC_USE_INTERLEAVED_THINKING) {
|
|
1568
|
+
return castApi<"anthropic-messages">({
|
|
1569
|
+
...base,
|
|
1570
|
+
requestModelId: resolveWireModelId(model, reasoning),
|
|
1571
|
+
thinkingEnabled: true,
|
|
1572
|
+
thinkingBudgetTokens: thinkingBudget,
|
|
1573
|
+
effort,
|
|
1574
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1575
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1576
|
+
serviceTier: options?.serviceTier,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
|
|
1581
|
+
const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
|
|
1582
|
+
|
|
1583
|
+
// If not enough room for thinking + output, reduce thinking budget
|
|
1584
|
+
if (maxTokens <= thinkingBudget) {
|
|
1585
|
+
thinkingBudget = maxTokens - MIN_OUTPUT_TOKENS;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// If thinking budget is too low, disable thinking
|
|
1589
|
+
if (thinkingBudget <= 0) {
|
|
1590
|
+
return castApi<"anthropic-messages">({
|
|
1591
|
+
...base,
|
|
1592
|
+
requestModelId: resolveWireModelId(model, undefined),
|
|
1593
|
+
thinkingEnabled: false,
|
|
1594
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1595
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1596
|
+
serviceTier: options?.serviceTier,
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
return castApi<"anthropic-messages">({
|
|
1600
|
+
...base,
|
|
1601
|
+
maxTokens,
|
|
1602
|
+
requestModelId: resolveWireModelId(model, reasoning),
|
|
1603
|
+
thinkingEnabled: true,
|
|
1604
|
+
thinkingBudgetTokens: thinkingBudget,
|
|
1605
|
+
effort,
|
|
1606
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1607
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1608
|
+
serviceTier: options?.serviceTier,
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
case "bedrock-converse-stream": {
|
|
1614
|
+
const bedrockBase: BedrockOptions = {
|
|
1615
|
+
...base,
|
|
1616
|
+
reasoning: options?.reasoning,
|
|
1617
|
+
thinkingBudgets: options?.thinkingBudgets,
|
|
1618
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1619
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1620
|
+
};
|
|
1621
|
+
// Adaptive mode sends effort directly, no budget_tokens — skip budget inflation.
|
|
1622
|
+
if (model.thinking?.mode === "anthropic-adaptive") {
|
|
1623
|
+
return castApi<"bedrock-converse-stream">(bedrockBase);
|
|
1624
|
+
}
|
|
1625
|
+
const budgetInfo = resolveBedrockThinkingBudget(model as Model<"bedrock-converse-stream">, options);
|
|
1626
|
+
if (!budgetInfo) return bedrockBase as OptionsForApi<TApi>;
|
|
1627
|
+
let maxTokens = bedrockBase.maxTokens ?? model.maxTokens ?? OUTPUT_CAP_WHEN_UNKNOWN;
|
|
1628
|
+
let thinkingBudgets = bedrockBase.thinkingBudgets;
|
|
1629
|
+
if (maxTokens <= budgetInfo.budget) {
|
|
1630
|
+
const desiredMaxTokens = Math.min(
|
|
1631
|
+
model.maxTokens ?? Number.POSITIVE_INFINITY,
|
|
1632
|
+
budgetInfo.budget + MIN_OUTPUT_TOKENS,
|
|
1633
|
+
);
|
|
1634
|
+
if (desiredMaxTokens > maxTokens) {
|
|
1635
|
+
maxTokens = desiredMaxTokens;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (maxTokens <= budgetInfo.budget) {
|
|
1639
|
+
const adjustedBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
|
|
1640
|
+
thinkingBudgets = { ...(thinkingBudgets ?? {}), [budgetInfo.level]: adjustedBudget };
|
|
1641
|
+
}
|
|
1642
|
+
return castApi<"bedrock-converse-stream">({ ...bedrockBase, maxTokens, thinkingBudgets });
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
case "openrouter": {
|
|
1646
|
+
const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
|
|
1647
|
+
if (useResponses) {
|
|
1648
|
+
return castApi<"openai-responses">({
|
|
1649
|
+
...base,
|
|
1650
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1651
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1652
|
+
serviceTier: options?.serviceTier,
|
|
1653
|
+
reasoningSummary: options?.hideThinkingSummary ? null : undefined,
|
|
1654
|
+
openrouterVariant: options?.openrouterVariant,
|
|
1655
|
+
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1656
|
+
disableReasoning: options?.disableReasoning,
|
|
1657
|
+
textVerbosity: options?.textVerbosity,
|
|
1658
|
+
promptCache: options?.promptCache,
|
|
1659
|
+
statefulResponses: options?.statefulResponses,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
return castApi<"openai-completions">({
|
|
1663
|
+
...base,
|
|
1664
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1665
|
+
disableReasoning: options?.disableReasoning,
|
|
1666
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1667
|
+
serviceTier: options?.serviceTier,
|
|
1668
|
+
openrouterVariant: options?.openrouterVariant,
|
|
1669
|
+
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1670
|
+
promptCache: options?.promptCache,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
case "openai-completions":
|
|
1675
|
+
return castApi<"openai-completions">({
|
|
1676
|
+
...base,
|
|
1677
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1678
|
+
disableReasoning: options?.disableReasoning,
|
|
1679
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1680
|
+
serviceTier: options?.serviceTier,
|
|
1681
|
+
openrouterVariant: options?.openrouterVariant,
|
|
1682
|
+
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1683
|
+
promptCache: options?.promptCache,
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
case "openai-responses":
|
|
1687
|
+
return castApi<"openai-responses">({
|
|
1688
|
+
...base,
|
|
1689
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1690
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1691
|
+
serviceTier: options?.serviceTier,
|
|
1692
|
+
reasoningSummary: options?.hideThinkingSummary ? null : undefined,
|
|
1693
|
+
openrouterVariant: options?.openrouterVariant,
|
|
1694
|
+
maxTokensExplicit: rawOptions?.maxTokens !== undefined,
|
|
1695
|
+
disableReasoning: options?.disableReasoning,
|
|
1696
|
+
textVerbosity: options?.textVerbosity,
|
|
1697
|
+
promptCache: options?.promptCache,
|
|
1698
|
+
statefulResponses: options?.statefulResponses,
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
case "azure-openai-responses":
|
|
1702
|
+
return castApi<"azure-openai-responses">({
|
|
1703
|
+
...base,
|
|
1704
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1705
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1706
|
+
serviceTier: options?.serviceTier,
|
|
1707
|
+
reasoningSummary: options?.hideThinkingSummary ? null : undefined,
|
|
1708
|
+
promptCache: options?.promptCache,
|
|
1709
|
+
statefulResponses: options?.statefulResponses,
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
case "openai-codex-responses":
|
|
1713
|
+
return castApi<"openai-codex-responses">({
|
|
1714
|
+
...base,
|
|
1715
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1716
|
+
toolChoice: mapOpenAiToolChoice(options?.toolChoice),
|
|
1717
|
+
serviceTier: options?.serviceTier,
|
|
1718
|
+
preferWebsockets: options?.preferWebsockets,
|
|
1719
|
+
codexCompaction: options?.codexCompaction,
|
|
1720
|
+
reasoningSummary: options?.hideThinkingSummary ? null : undefined,
|
|
1721
|
+
textVerbosity: options?.textVerbosity,
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
case "google-generative-ai": {
|
|
1725
|
+
// Explicitly disable thinking when reasoning is not specified or model doesn't support it
|
|
1726
|
+
// This is needed because Gemini has "dynamic thinking" enabled by default
|
|
1727
|
+
const reasoning = options?.reasoning;
|
|
1728
|
+
if (!reasoning || !model.reasoning) {
|
|
1729
|
+
return castApi<"google-generative-ai">({
|
|
1730
|
+
...base,
|
|
1731
|
+
serviceTier: options?.serviceTier,
|
|
1732
|
+
thinking: { enabled: false },
|
|
1733
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1734
|
+
cachedContent: options?.cachedContent,
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const googleModel = model as Model<"google-generative-ai">;
|
|
1739
|
+
const effort = requireSupportedEffort(googleModel, reasoning);
|
|
1740
|
+
|
|
1741
|
+
// Gemini 3+ models use thinkingLevel exclusively instead of thinkingBudget.
|
|
1742
|
+
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
|
1743
|
+
if (googleModel.thinking?.mode === "google-level") {
|
|
1744
|
+
return castApi<"google-generative-ai">({
|
|
1745
|
+
...base,
|
|
1746
|
+
serviceTier: options?.serviceTier,
|
|
1747
|
+
thinking: {
|
|
1748
|
+
enabled: true,
|
|
1749
|
+
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1750
|
+
},
|
|
1751
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1752
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1753
|
+
cachedContent: options?.cachedContent,
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return castApi<"google-generative-ai">({
|
|
1758
|
+
...base,
|
|
1759
|
+
thinking: {
|
|
1760
|
+
enabled: true,
|
|
1761
|
+
budgetTokens: getGoogleBudget(googleModel, effort, options?.thinkingBudgets),
|
|
1762
|
+
},
|
|
1763
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1764
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1765
|
+
cachedContent: options?.cachedContent,
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
case "google-gemini-cli": {
|
|
1770
|
+
const reasoning = options?.reasoning;
|
|
1771
|
+
const toolChoice = mapGoogleToolChoice(options?.toolChoice);
|
|
1772
|
+
if (reasoning && model.reasoning) {
|
|
1773
|
+
const effort = requireSupportedEffort(model, reasoning);
|
|
1774
|
+
|
|
1775
|
+
// Gemini 3+ models use thinkingLevel instead of thinkingBudget
|
|
1776
|
+
if (model.thinking?.mode === "google-level") {
|
|
1777
|
+
return castApi<"google-gemini-cli">({
|
|
1778
|
+
...base,
|
|
1779
|
+
requestModelId: resolveWireModelId(model, effort),
|
|
1780
|
+
thinking: {
|
|
1781
|
+
enabled: true,
|
|
1782
|
+
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1783
|
+
},
|
|
1784
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1785
|
+
toolChoice,
|
|
1786
|
+
antigravityEndpointMode: options?.antigravityEndpointMode,
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
let thinkingBudget =
|
|
1791
|
+
options.thinkingBudgets?.[effort] ?? model.thinking?.effortBudgets?.[effort] ?? GOOGLE_THINKING[effort];
|
|
1792
|
+
|
|
1793
|
+
// Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
|
|
1794
|
+
const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
|
|
1795
|
+
|
|
1796
|
+
// If not enough room for thinking + output, reduce thinking budget
|
|
1797
|
+
if (maxTokens <= thinkingBudget) {
|
|
1798
|
+
thinkingBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (thinkingBudget > 0) {
|
|
1802
|
+
return castApi<"google-gemini-cli">({
|
|
1803
|
+
...base,
|
|
1804
|
+
maxTokens,
|
|
1805
|
+
requestModelId: resolveWireModelId(model, effort),
|
|
1806
|
+
thinking: { enabled: true, budgetTokens: thinkingBudget },
|
|
1807
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1808
|
+
toolChoice,
|
|
1809
|
+
antigravityEndpointMode: options?.antigravityEndpointMode,
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
// Budget clamped to zero — fall through to the thinking-off path.
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const thinking: GoogleGeminiCliOptions["thinking"] = { enabled: false };
|
|
1816
|
+
if (model.reasoning && model.thinking?.suppressWhenOff) {
|
|
1817
|
+
// CCA re-applies the per-id baked server default when the config
|
|
1818
|
+
// is omitted; suppression must be explicit on the wire.
|
|
1819
|
+
thinking.suppress = model.thinking.mode === "google-level" ? { level: "MINIMAL" } : { budget: 0 };
|
|
1820
|
+
}
|
|
1821
|
+
return castApi<"google-gemini-cli">({
|
|
1822
|
+
...base,
|
|
1823
|
+
requestModelId: resolveWireModelId(model, undefined),
|
|
1824
|
+
thinking,
|
|
1825
|
+
toolChoice,
|
|
1826
|
+
antigravityEndpointMode: options?.antigravityEndpointMode,
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
case "google-vertex": {
|
|
1831
|
+
// Explicitly disable thinking when reasoning is not specified or model doesn't support it
|
|
1832
|
+
const reasoning = options?.reasoning;
|
|
1833
|
+
if (!reasoning || !model.reasoning) {
|
|
1834
|
+
return castApi<"google-vertex">({
|
|
1835
|
+
...base,
|
|
1836
|
+
serviceTier: options?.serviceTier,
|
|
1837
|
+
thinking: { enabled: false },
|
|
1838
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1839
|
+
cachedContent: options?.cachedContent,
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const vertexModel = model as Model<"google-vertex">;
|
|
1844
|
+
const effort = requireSupportedEffort(vertexModel, reasoning);
|
|
1845
|
+
const geminiModel = vertexModel as unknown as Model<"google-generative-ai">;
|
|
1846
|
+
|
|
1847
|
+
if (geminiModel.thinking?.mode === "google-level") {
|
|
1848
|
+
return castApi<"google-vertex">({
|
|
1849
|
+
...base,
|
|
1850
|
+
serviceTier: options?.serviceTier,
|
|
1851
|
+
thinking: {
|
|
1852
|
+
enabled: true,
|
|
1853
|
+
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1854
|
+
},
|
|
1855
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1856
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1857
|
+
cachedContent: options?.cachedContent,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return castApi<"google-vertex">({
|
|
1862
|
+
...base,
|
|
1863
|
+
serviceTier: options?.serviceTier,
|
|
1864
|
+
thinking: {
|
|
1865
|
+
enabled: true,
|
|
1866
|
+
budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
|
|
1867
|
+
},
|
|
1868
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1869
|
+
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1870
|
+
cachedContent: options?.cachedContent,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
case "ollama-chat":
|
|
1875
|
+
return castApi<"ollama-chat">({
|
|
1876
|
+
...base,
|
|
1877
|
+
reasoning: resolveOpenAiReasoningEffort(model, options),
|
|
1878
|
+
disableReasoning: options?.disableReasoning,
|
|
1879
|
+
toolChoice: options?.toolChoice,
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
case "cursor-agent": {
|
|
1883
|
+
const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
|
|
1884
|
+
const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
|
|
1885
|
+
return castApi<"cursor-agent">({
|
|
1886
|
+
...base,
|
|
1887
|
+
execHandlers,
|
|
1888
|
+
onToolResult,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
case "gitlab-duo-agent":
|
|
1893
|
+
return castApi<"gitlab-duo-agent">({
|
|
1894
|
+
...base,
|
|
1895
|
+
cwd: options?.cwd,
|
|
1896
|
+
toolChoice: options?.toolChoice,
|
|
1897
|
+
});
|
|
1898
|
+
case "devin-agent": {
|
|
1899
|
+
const devinModel = model as Model<"devin-agent">;
|
|
1900
|
+
const effort =
|
|
1901
|
+
options?.reasoning && !options.disableReasoning
|
|
1902
|
+
? requireSupportedEffort(devinModel, options.reasoning)
|
|
1903
|
+
: undefined;
|
|
1904
|
+
return castApi<"devin-agent">({
|
|
1905
|
+
...base,
|
|
1906
|
+
chatModelUid: resolveWireModelId(devinModel, effort),
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
default:
|
|
1910
|
+
throw new AIError.ConfigurationError(`Unhandled API in mapOptionsForApi: ${model.api}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function getGoogleBudget(
|
|
1915
|
+
model: Model<"google-generative-ai">,
|
|
1916
|
+
effort: Effort,
|
|
1917
|
+
customBudgets?: ThinkingBudgets,
|
|
1918
|
+
): number {
|
|
1919
|
+
requireSupportedEffort(model, effort);
|
|
1920
|
+
|
|
1921
|
+
// Custom budgets take precedence if provided for this level
|
|
1922
|
+
if (customBudgets?.[effort] !== undefined) {
|
|
1923
|
+
return customBudgets[effort]!;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// See https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
|
1927
|
+
if (model.id.includes("2.5-")) {
|
|
1928
|
+
switch (effort) {
|
|
1929
|
+
case "minimal":
|
|
1930
|
+
return 128;
|
|
1931
|
+
case "low":
|
|
1932
|
+
return 2048;
|
|
1933
|
+
case "medium":
|
|
1934
|
+
return 8192;
|
|
1935
|
+
case "high":
|
|
1936
|
+
case "xhigh":
|
|
1937
|
+
case "max":
|
|
1938
|
+
return model.id.includes("2.5-flash") ? 24576 : 32768;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Unknown model - use dynamic
|
|
1943
|
+
return -1;
|
|
1944
|
+
}
|