@linxiraos/pi-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5066 -0
- package/README.md +1195 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
- package/dist/types/auth-broker/client.d.ts +93 -0
- package/dist/types/auth-broker/discover.d.ts +39 -0
- package/dist/types/auth-broker/index.d.ts +7 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +136 -0
- package/dist/types/auth-broker/server.d.ts +48 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +152 -0
- package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
- package/dist/types/auth-gateway/http.d.ts +56 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +36 -0
- package/dist/types/auth-gateway/types.d.ts +127 -0
- package/dist/types/auth-retry.d.ts +150 -0
- package/dist/types/auth-storage.d.ts +1258 -0
- package/dist/types/dialect/anthropic.d.ts +15 -0
- package/dist/types/dialect/catalog.d.ts +3 -0
- package/dist/types/dialect/coercion.d.ts +23 -0
- package/dist/types/dialect/deepseek.d.ts +14 -0
- package/dist/types/dialect/demotion.d.ts +30 -0
- package/dist/types/dialect/examples.d.ts +18 -0
- package/dist/types/dialect/factory.d.ts +3 -0
- package/dist/types/dialect/fenced-thinking.d.ts +53 -0
- package/dist/types/dialect/gemini.d.ts +17 -0
- package/dist/types/dialect/gemma.d.ts +15 -0
- package/dist/types/dialect/glm.d.ts +9 -0
- package/dist/types/dialect/harmony.d.ts +8 -0
- package/dist/types/dialect/hermes.d.ts +9 -0
- package/dist/types/dialect/history.d.ts +3 -0
- package/dist/types/dialect/index.d.ts +11 -0
- package/dist/types/dialect/inventory.d.ts +9 -0
- package/dist/types/dialect/kimi.d.ts +14 -0
- package/dist/types/dialect/minimax.d.ts +3 -0
- package/dist/types/dialect/owned-stream.d.ts +4 -0
- package/dist/types/dialect/qwen3.d.ts +9 -0
- package/dist/types/dialect/rendering.d.ts +54 -0
- package/dist/types/dialect/thinking.d.ts +6 -0
- package/dist/types/dialect/types.d.ts +68 -0
- package/dist/types/dialect/xml.d.ts +9 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +20 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +27 -0
- package/dist/types/error/classes.d.ts +106 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +94 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +14 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/error/rate-limit.d.ts +80 -0
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/providers/amazon-bedrock.d.ts +39 -0
- package/dist/types/providers/anthropic-client.d.ts +106 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic-wire.d.ts +345 -0
- package/dist/types/providers/anthropic.d.ts +261 -0
- package/dist/types/providers/aws-credentials.d.ts +48 -0
- package/dist/types/providers/aws-eventstream.d.ts +39 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +16 -0
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
- package/dist/types/providers/cowork-fetch.d.ts +3 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor-pi-args.d.ts +105 -0
- package/dist/types/providers/cursor.d.ts +221 -0
- package/dist/types/providers/devin.d.ts +12 -0
- package/dist/types/providers/error-message.d.ts +25 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +120 -0
- package/dist/types/providers/google-shared.d.ts +203 -0
- package/dist/types/providers/google-types.d.ts +155 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +179 -0
- package/dist/types/providers/ollama.d.ts +8 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-chat-wire.d.ts +669 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
- package/dist/types/providers/openai-codex-responses.d.ts +238 -0
- package/dist/types/providers/openai-completions.d.ts +48 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
- package/dist/types/providers/openai-responses.d.ts +131 -0
- package/dist/types/providers/openai-shared.d.ts +624 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +69 -0
- package/dist/types/providers/register-builtins.d.ts +37 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +32 -0
- package/dist/types/providers/vision-guard.d.ts +20 -0
- package/dist/types/registry/aiand.d.ts +7 -0
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
- package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/registry/api-key-login.d.ts +42 -0
- package/dist/types/registry/api-key-validation.d.ts +43 -0
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/azure.d.ts +4 -0
- package/dist/types/registry/baseten.d.ts +7 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/cerebras.d.ts +7 -0
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
- package/dist/types/registry/coreweave.d.ts +7 -0
- package/dist/types/registry/cursor.d.ts +7 -0
- package/dist/types/registry/deepseek.d.ts +8 -0
- package/dist/types/registry/derived.d.ts +5 -0
- package/dist/types/registry/devin.d.ts +8 -0
- package/dist/types/registry/exa.d.ts +8 -0
- package/dist/types/registry/firepass.d.ts +16 -0
- package/dist/types/registry/fireworks.d.ts +7 -0
- package/dist/types/registry/github-copilot.d.ts +7 -0
- package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
- package/dist/types/registry/gitlab-duo.d.ts +9 -0
- package/dist/types/registry/gmi-cloud.d.ts +7 -0
- package/dist/types/registry/google-antigravity.d.ts +9 -0
- package/dist/types/registry/google-gemini-cli.d.ts +9 -0
- package/dist/types/registry/google-vertex.d.ts +5 -0
- package/dist/types/registry/google.d.ts +4 -0
- package/dist/types/registry/groq.d.ts +4 -0
- package/dist/types/registry/huggingface.d.ts +7 -0
- package/dist/types/registry/index.d.ts +4 -0
- package/dist/types/registry/kagi.d.ts +14 -0
- package/dist/types/registry/kilo.d.ts +7 -0
- package/dist/types/registry/kimi-code.d.ts +7 -0
- package/dist/types/registry/litellm.d.ts +13 -0
- package/dist/types/registry/llama-cpp.d.ts +8 -0
- package/dist/types/registry/lm-studio.d.ts +8 -0
- package/dist/types/registry/meta.d.ts +7 -0
- package/dist/types/registry/minimax-code-cn.d.ts +6 -0
- package/dist/types/registry/minimax-code.d.ts +6 -0
- package/dist/types/registry/minimax.d.ts +4 -0
- package/dist/types/registry/mistral.d.ts +4 -0
- package/dist/types/registry/moonshot.d.ts +7 -0
- package/dist/types/registry/nanogpt.d.ts +7 -0
- package/dist/types/registry/novita.d.ts +6 -0
- package/dist/types/registry/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
- package/dist/types/registry/oauth/anthropic.d.ts +24 -0
- package/dist/types/registry/oauth/callback-server.d.ts +74 -0
- package/dist/types/registry/oauth/cursor.d.ts +16 -0
- package/dist/types/registry/oauth/device-code.d.ts +25 -0
- package/dist/types/registry/oauth/devin.d.ts +5 -0
- package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
- package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
- package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
- package/dist/types/registry/oauth/index.d.ts +45 -0
- package/dist/types/registry/oauth/kimi.d.ts +21 -0
- package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
- package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
- package/dist/types/registry/oauth/opencode.d.ts +18 -0
- package/dist/types/registry/oauth/perplexity.d.ts +9 -0
- package/dist/types/registry/oauth/pkce.d.ts +8 -0
- package/dist/types/registry/oauth/types.d.ts +87 -0
- package/dist/types/registry/oauth/wafer.d.ts +1 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
- package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
- package/dist/types/registry/oauth/zai.d.ts +25 -0
- package/dist/types/registry/ollama-cloud.d.ts +7 -0
- package/dist/types/registry/ollama.d.ts +12 -0
- package/dist/types/registry/openai-codex-device.d.ts +8 -0
- package/dist/types/registry/openai-codex.d.ts +9 -0
- package/dist/types/registry/openai.d.ts +4 -0
- package/dist/types/registry/opencode-go.d.ts +6 -0
- package/dist/types/registry/opencode-zen.d.ts +6 -0
- package/dist/types/registry/openrouter.d.ts +13 -0
- package/dist/types/registry/parallel.d.ts +14 -0
- package/dist/types/registry/perplexity.d.ts +7 -0
- package/dist/types/registry/qianfan.d.ts +7 -0
- package/dist/types/registry/qwen-portal.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +367 -0
- package/dist/types/registry/sakana.d.ts +7 -0
- package/dist/types/registry/siliconflow-cn.d.ts +7 -0
- package/dist/types/registry/siliconflow.d.ts +7 -0
- package/dist/types/registry/synthetic.d.ts +6 -0
- package/dist/types/registry/tavily.d.ts +14 -0
- package/dist/types/registry/together.d.ts +6 -0
- package/dist/types/registry/types.d.ts +75 -0
- package/dist/types/registry/umans.d.ts +7 -0
- package/dist/types/registry/venice.d.ts +13 -0
- package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
- package/dist/types/registry/vllm.d.ts +7 -0
- package/dist/types/registry/wafer-serverless.d.ts +6 -0
- package/dist/types/registry/xai-oauth.d.ts +7 -0
- package/dist/types/registry/xai.d.ts +7 -0
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
- package/dist/types/registry/xiaomi.d.ts +6 -0
- package/dist/types/registry/zai.d.ts +15 -0
- package/dist/types/registry/zenmux.d.ts +7 -0
- package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
- package/dist/types/stream.d.ts +46 -0
- package/dist/types/types.d.ts +1064 -0
- package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
- package/dist/types/usage/claude.d.ts +4 -0
- package/dist/types/usage/cursor.d.ts +4 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +15 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +3 -0
- package/dist/types/usage/ollama.d.ts +5 -0
- package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
- package/dist/types/usage/openai-codex-reset.d.ts +88 -0
- package/dist/types/usage/openai-codex.d.ts +10 -0
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/synthetic.d.ts +2 -0
- package/dist/types/usage/umans.d.ts +2 -0
- package/dist/types/usage/xai-oauth.d.ts +12 -0
- package/dist/types/usage/zai.d.ts +3 -0
- package/dist/types/usage.d.ts +527 -0
- package/dist/types/utils/abort.d.ts +25 -0
- package/dist/types/utils/anthropic-auth.d.ts +35 -0
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/block-symbols.d.ts +62 -0
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/empty-completion-retry.d.ts +21 -0
- package/dist/types/utils/event-stream.d.ts +39 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/google-validation.d.ts +2 -0
- package/dist/types/utils/harmony-leak.d.ts +135 -0
- package/dist/types/utils/http-inspector.d.ts +49 -0
- package/dist/types/utils/idle-iterator.d.ts +149 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
- package/dist/types/utils/openai-http.d.ts +48 -0
- package/dist/types/utils/openrouter-headers.d.ts +1 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/proxy.d.ts +39 -0
- package/dist/types/utils/request-debug.d.ts +29 -0
- package/dist/types/utils/retry-after.d.ts +4 -0
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +54 -0
- package/dist/types/utils/schema/index.d.ts +14 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +153 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +17 -0
- package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/typescript.d.ts +24 -0
- package/dist/types/utils/schema/wire.d.ts +52 -0
- package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
- package/dist/types/utils/sse-debug.d.ts +5 -0
- package/dist/types/utils/stream-markup-healing.d.ts +87 -0
- package/dist/types/utils/thinking-loop.d.ts +102 -0
- package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
- package/dist/types/utils/tool-choice.d.ts +52 -0
- package/dist/types/utils/validation.d.ts +28 -0
- package/dist/types/utils.d.ts +57 -0
- package/package.json +138 -0
- package/src/api-registry.ts +109 -0
- package/src/auth/sqlite-credential-store.ts +2066 -0
- package/src/auth-broker/client.ts +471 -0
- package/src/auth-broker/discover.ts +310 -0
- package/src/auth-broker/index.ts +7 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +1332 -0
- package/src/auth-broker/server.ts +898 -0
- package/src/auth-broker/snapshot-cache.ts +200 -0
- package/src/auth-broker/types.ts +193 -0
- package/src/auth-broker/wire-schema-resource.ts +487 -0
- package/src/auth-broker/wire-schemas.ts +43 -0
- package/src/auth-gateway/http.ts +227 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +836 -0
- package/src/auth-gateway/types.ts +153 -0
- package/src/auth-retry.ts +401 -0
- package/src/auth-storage.ts +6540 -0
- package/src/dialect/anthropic.md +31 -0
- package/src/dialect/anthropic.ts +608 -0
- package/src/dialect/catalog.ts +29 -0
- package/src/dialect/coercion.ts +136 -0
- package/src/dialect/deepseek.md +24 -0
- package/src/dialect/deepseek.ts +609 -0
- package/src/dialect/demotion.ts +40 -0
- package/src/dialect/examples.ts +71 -0
- package/src/dialect/factory.ts +34 -0
- package/src/dialect/fenced-thinking.ts +184 -0
- package/src/dialect/gemini.md +44 -0
- package/src/dialect/gemini.ts +583 -0
- package/src/dialect/gemma.md +33 -0
- package/src/dialect/gemma.ts +387 -0
- package/src/dialect/glm.md +32 -0
- package/src/dialect/glm.ts +579 -0
- package/src/dialect/harmony.md +31 -0
- package/src/dialect/harmony.ts +345 -0
- package/src/dialect/hermes.md +25 -0
- package/src/dialect/hermes.ts +206 -0
- package/src/dialect/history.ts +81 -0
- package/src/dialect/index.ts +15 -0
- package/src/dialect/inventory.ts +30 -0
- package/src/dialect/kimi.md +24 -0
- package/src/dialect/kimi.ts +340 -0
- package/src/dialect/minimax.md +31 -0
- package/src/dialect/minimax.ts +95 -0
- package/src/dialect/owned-stream.ts +481 -0
- package/src/dialect/prompt-template.md +12 -0
- package/src/dialect/qwen3.md +28 -0
- package/src/dialect/qwen3.ts +240 -0
- package/src/dialect/rendering.ts +304 -0
- package/src/dialect/thinking.ts +292 -0
- package/src/dialect/types.ts +56 -0
- package/src/dialect/xml.md +22 -0
- package/src/dialect/xml.ts +90 -0
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +47 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +35 -0
- package/src/error/classes.ts +281 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +602 -0
- package/src/error/format.ts +45 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +14 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +63 -0
- package/src/error/rate-limit.ts +303 -0
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +51 -0
- package/src/provider-details.ts +90 -0
- package/src/providers/amazon-bedrock.ts +1064 -0
- package/src/providers/anthropic-client.ts +317 -0
- package/src/providers/anthropic-messages-server-schema.ts +252 -0
- package/src/providers/anthropic-messages-server.ts +818 -0
- package/src/providers/anthropic-wire.ts +359 -0
- package/src/providers/anthropic.ts +4539 -0
- package/src/providers/aws-credentials.ts +772 -0
- package/src/providers/aws-eventstream.ts +181 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +438 -0
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/claude-code-fingerprint.ts +20 -0
- package/src/providers/cowork-fetch.ts +201 -0
- package/src/providers/cursor/exec-modern.ts +496 -0
- package/src/providers/cursor/proto/agent.proto +4533 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor-pi-args.ts +165 -0
- package/src/providers/cursor.ts +4689 -0
- package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
- package/src/providers/devin/proto/buf.gen.yaml +33 -0
- package/src/providers/devin/proto/buf.yaml +17 -0
- package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
- package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
- package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
- package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
- package/src/providers/devin/proto/cel/expr/value.proto +41 -0
- package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
- package/src/providers/devin/proto/errorspb/errors.proto +56 -0
- package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
- package/src/providers/devin/proto/errorspb/markers.proto +10 -0
- package/src/providers/devin/proto/errorspb/tags.proto +12 -0
- package/src/providers/devin/proto/errorspb/testing.proto +6 -0
- package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
- package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
- package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
- package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
- package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
- package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
- package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
- package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
- package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
- package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
- package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
- package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
- package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
- package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
- package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
- package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
- package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
- package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
- package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
- package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
- package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
- package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
- package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
- package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
- package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
- package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
- package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
- package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
- package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
- package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
- package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
- package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
- package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
- package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
- package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
- package/src/providers/devin.ts +679 -0
- package/src/providers/error-message.ts +23 -0
- package/src/providers/github-copilot-headers.ts +141 -0
- package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
- package/src/providers/gitlab-duo-workflow.ts +3135 -0
- package/src/providers/gitlab-duo.ts +399 -0
- package/src/providers/google-auth.ts +330 -0
- package/src/providers/google-gemini-cli.ts +1370 -0
- package/src/providers/google-shared.ts +1122 -0
- package/src/providers/google-types.ts +180 -0
- package/src/providers/google-vertex.ts +135 -0
- package/src/providers/google.ts +47 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +51 -0
- package/src/providers/mock.ts +514 -0
- package/src/providers/ollama.ts +776 -0
- package/src/providers/openai-anthropic-shim.ts +166 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +752 -0
- package/src/providers/openai-chat-wire.ts +859 -0
- package/src/providers/openai-codex/request-transformer.ts +491 -0
- package/src/providers/openai-codex/response-handler.ts +102 -0
- package/src/providers/openai-codex-responses.ts +4716 -0
- package/src/providers/openai-completions.ts +2389 -0
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses-server-schema.ts +397 -0
- package/src/providers/openai-responses-server.ts +1466 -0
- package/src/providers/openai-responses-wire.ts +6416 -0
- package/src/providers/openai-responses.ts +1393 -0
- package/src/providers/openai-shared.ts +3500 -0
- package/src/providers/pi-native-client.ts +275 -0
- package/src/providers/pi-native-server.ts +245 -0
- package/src/providers/register-builtins.ts +503 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +1083 -0
- package/src/providers/vision-guard.ts +54 -0
- package/src/registry/aiand.ts +22 -0
- package/src/registry/aimlapi.ts +6 -0
- package/src/registry/alibaba-coding-plan.ts +104 -0
- package/src/registry/alibaba-token-plan.ts +125 -0
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/registry/api-key-login.ts +115 -0
- package/src/registry/api-key-validation.ts +145 -0
- package/src/registry/aws.ts +57 -0
- package/src/registry/azure.ts +6 -0
- package/src/registry/baseten.ts +22 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/cerebras.ts +23 -0
- package/src/registry/cloudflare-ai-gateway.ts +45 -0
- package/src/registry/coreweave.ts +40 -0
- package/src/registry/cursor.ts +20 -0
- package/src/registry/deepseek.ts +46 -0
- package/src/registry/derived.ts +9 -0
- package/src/registry/devin.ts +15 -0
- package/src/registry/exa.ts +19 -0
- package/src/registry/firepass.ts +32 -0
- package/src/registry/fireworks.ts +28 -0
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo-workflow.ts +20 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/gmi-cloud.ts +22 -0
- package/src/registry/google-antigravity.ts +22 -0
- package/src/registry/google-gemini-cli.ts +22 -0
- package/src/registry/google-vertex.ts +38 -0
- package/src/registry/google.ts +6 -0
- package/src/registry/groq.ts +6 -0
- package/src/registry/huggingface.ts +29 -0
- package/src/registry/index.ts +4 -0
- package/src/registry/kagi.ts +46 -0
- package/src/registry/kilo.ts +114 -0
- package/src/registry/kimi-code.ts +17 -0
- package/src/registry/litellm.ts +45 -0
- package/src/registry/llama-cpp.ts +35 -0
- package/src/registry/lm-studio.ts +31 -0
- package/src/registry/meta.ts +22 -0
- package/src/registry/minimax-code-cn.ts +12 -0
- package/src/registry/minimax-code.ts +12 -0
- package/src/registry/minimax.ts +6 -0
- package/src/registry/mistral.ts +6 -0
- package/src/registry/moonshot.ts +28 -0
- package/src/registry/nanogpt.ts +22 -0
- package/src/registry/novita.ts +25 -0
- package/src/registry/nvidia.ts +61 -0
- package/src/registry/oauth/anthropic-constants.ts +12 -0
- package/src/registry/oauth/anthropic.ts +346 -0
- package/src/registry/oauth/callback-server.ts +438 -0
- package/src/registry/oauth/cursor.ts +187 -0
- package/src/registry/oauth/device-code.ts +92 -0
- package/src/registry/oauth/devin.ts +124 -0
- package/src/registry/oauth/github-copilot.ts +369 -0
- package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
- package/src/registry/oauth/gitlab-duo.ts +222 -0
- package/src/registry/oauth/google-antigravity.ts +225 -0
- package/src/registry/oauth/google-gemini-cli.ts +297 -0
- package/src/registry/oauth/google-oauth-shared.ts +211 -0
- package/src/registry/oauth/index.ts +187 -0
- package/src/registry/oauth/kimi.ts +297 -0
- package/src/registry/oauth/minimax-code.ts +53 -0
- package/src/registry/oauth/oauth.html +317 -0
- package/src/registry/oauth/openai-codex.ts +384 -0
- package/src/registry/oauth/opencode.ts +50 -0
- package/src/registry/oauth/perplexity.ts +228 -0
- package/src/registry/oauth/pkce.ts +18 -0
- package/src/registry/oauth/types.ts +96 -0
- package/src/registry/oauth/wafer.ts +24 -0
- package/src/registry/oauth/xai-oauth.ts +559 -0
- package/src/registry/oauth/xiaomi.ts +211 -0
- package/src/registry/oauth/zai.ts +285 -0
- package/src/registry/ollama-cloud.ts +36 -0
- package/src/registry/ollama.ts +43 -0
- package/src/registry/openai-codex-device.ts +18 -0
- package/src/registry/openai-codex.ts +19 -0
- package/src/registry/openai.ts +6 -0
- package/src/registry/opencode-go.ts +12 -0
- package/src/registry/opencode-zen.ts +12 -0
- package/src/registry/openrouter.ts +28 -0
- package/src/registry/parallel.ts +45 -0
- package/src/registry/perplexity.ts +13 -0
- package/src/registry/qianfan.ts +27 -0
- package/src/registry/qwen-portal.ts +50 -0
- package/src/registry/registry.ts +182 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/siliconflow-cn.ts +22 -0
- package/src/registry/siliconflow.ts +22 -0
- package/src/registry/synthetic.ts +21 -0
- package/src/registry/tavily.ts +45 -0
- package/src/registry/together.ts +22 -0
- package/src/registry/types.ts +86 -0
- package/src/registry/umans.ts +23 -0
- package/src/registry/venice.ts +33 -0
- package/src/registry/vercel-ai-gateway.ts +38 -0
- package/src/registry/vllm.ts +34 -0
- package/src/registry/wafer-serverless.ts +12 -0
- package/src/registry/xai-oauth.ts +17 -0
- package/src/registry/xai.ts +22 -0
- package/src/registry/xiaomi-token-plan-ams.ts +12 -0
- package/src/registry/xiaomi-token-plan-cn.ts +12 -0
- package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
- package/src/registry/xiaomi.ts +12 -0
- package/src/registry/zai.ts +41 -0
- package/src/registry/zenmux.ts +22 -0
- package/src/registry/zhipu-coding-plan.ts +27 -0
- package/src/stream.ts +1944 -0
- package/src/types.ts +1243 -0
- package/src/usage/alibaba-token-plan.ts +230 -0
- package/src/usage/claude.ts +830 -0
- package/src/usage/cursor.ts +335 -0
- package/src/usage/gemini.ts +258 -0
- package/src/usage/github-copilot.ts +424 -0
- package/src/usage/google-antigravity.ts +497 -0
- package/src/usage/kimi.ts +277 -0
- package/src/usage/minimax-code.ts +291 -0
- package/src/usage/ollama.ts +41 -0
- package/src/usage/openai-codex-base-url.ts +35 -0
- package/src/usage/openai-codex-reset.ts +205 -0
- package/src/usage/openai-codex.ts +627 -0
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/synthetic.ts +180 -0
- package/src/usage/umans.ts +192 -0
- package/src/usage/xai-oauth.ts +414 -0
- package/src/usage/zai.ts +370 -0
- package/src/usage.ts +411 -0
- package/src/utils/abort.ts +67 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/block-symbols.ts +78 -0
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/empty-completion-retry.ts +161 -0
- package/src/utils/event-stream.ts +202 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/google-validation.ts +25 -0
- package/src/utils/harmony-leak.ts +500 -0
- package/src/utils/http-inspector.ts +196 -0
- package/src/utils/idle-iterator.ts +531 -0
- package/src/utils/leaked-thinking-stream.ts +483 -0
- package/src/utils/openai-http.ts +119 -0
- package/src/utils/openrouter-headers.ts +12 -0
- package/src/utils/parse-bind.ts +56 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/proxy.ts +314 -0
- package/src/utils/request-debug.ts +351 -0
- package/src/utils/retry-after.ts +121 -0
- package/src/utils/retry.ts +77 -0
- package/src/utils/schema/CONSTRAINTS.md +168 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +210 -0
- package/src/utils/schema/index.ts +14 -0
- package/src/utils/schema/json-schema-validator.ts +595 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +2314 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +109 -0
- package/src/utils/schema/strict-tool-validation.ts +117 -0
- package/src/utils/schema/types.ts +10 -0
- package/src/utils/schema/typescript.ts +212 -0
- package/src/utils/schema/wire.ts +662 -0
- package/src/utils/sdk-stream-timeout.ts +43 -0
- package/src/utils/sse-debug.ts +18 -0
- package/src/utils/stream-markup-healing.ts +247 -0
- package/src/utils/thinking-loop.ts +552 -0
- package/src/utils/tool-call-loop-guard.ts +107 -0
- package/src/utils/tool-choice.ts +101 -0
- package/src/utils/validation.ts +1932 -0
- package/src/utils.ts +492 -0
|
@@ -0,0 +1,4539 @@
|
|
|
1
|
+
import * as nodeCrypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { scheduler } from "node:timers/promises";
|
|
4
|
+
import * as tls from "node:tls";
|
|
5
|
+
import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@linxiraos/pi-catalog/compat/anthropic";
|
|
6
|
+
import { hostMatchesUrl, isVertexRawPredictUrl } from "@linxiraos/pi-catalog/hosts";
|
|
7
|
+
import { mapEffortToAnthropicAdaptiveEffort } from "@linxiraos/pi-catalog/model-thinking";
|
|
8
|
+
import { calculateCost, getBundledModel } from "@linxiraos/pi-catalog/models";
|
|
9
|
+
import { isAnthropicOAuthToken } from "@linxiraos/pi-catalog/utils";
|
|
10
|
+
import { parseGitHubCopilotApiKey } from "@linxiraos/pi-catalog/wire/github-copilot";
|
|
11
|
+
import {
|
|
12
|
+
$env,
|
|
13
|
+
getInstallId,
|
|
14
|
+
isEnoent,
|
|
15
|
+
logger,
|
|
16
|
+
parseJsonWithRepair,
|
|
17
|
+
parseStreamingJsonThrottled,
|
|
18
|
+
readSseEvents,
|
|
19
|
+
} from "@linxiraos/pi-utils";
|
|
20
|
+
import { renderDemotedThinking } from "../dialect/demotion";
|
|
21
|
+
import * as AIError from "../error";
|
|
22
|
+
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
23
|
+
import type {
|
|
24
|
+
AnthropicFallbackContent,
|
|
25
|
+
AnthropicServerToolContent,
|
|
26
|
+
Api,
|
|
27
|
+
AssistantMessage,
|
|
28
|
+
CacheRetention,
|
|
29
|
+
Context,
|
|
30
|
+
FetchImpl,
|
|
31
|
+
ImageContent,
|
|
32
|
+
Message,
|
|
33
|
+
Model,
|
|
34
|
+
ProviderSessionState,
|
|
35
|
+
RawSseEvent,
|
|
36
|
+
RedactedThinkingContent,
|
|
37
|
+
ServiceTier,
|
|
38
|
+
SimpleStreamOptions,
|
|
39
|
+
StopReason,
|
|
40
|
+
StreamFunction,
|
|
41
|
+
StreamOptions,
|
|
42
|
+
TextContent,
|
|
43
|
+
ThinkingContent,
|
|
44
|
+
Tool,
|
|
45
|
+
ToolCall,
|
|
46
|
+
ToolResultMessage,
|
|
47
|
+
Usage,
|
|
48
|
+
} from "../types";
|
|
49
|
+
import { isRecord, normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
50
|
+
import { createAbortSourceTracker } from "../utils/abort";
|
|
51
|
+
import {
|
|
52
|
+
clearStreamingPartialJson,
|
|
53
|
+
kStreamingBlockIndex,
|
|
54
|
+
kStreamingLastParseLen,
|
|
55
|
+
kStreamingPartialJson,
|
|
56
|
+
} from "../utils/block-symbols";
|
|
57
|
+
import { withEmptyCompletionRetry } from "../utils/empty-completion-retry";
|
|
58
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
59
|
+
import { isFoundryEnabled } from "../utils/foundry";
|
|
60
|
+
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
61
|
+
import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
62
|
+
import { notifyProviderResponse } from "../utils/provider-response";
|
|
63
|
+
import { getHeadersFromError, getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
64
|
+
import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
65
|
+
import { spillToDescription } from "../utils/schema/spill";
|
|
66
|
+
import { createSdkStreamRequestOptions } from "../utils/sdk-stream-timeout";
|
|
67
|
+
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
68
|
+
import { isForcedToolChoice } from "../utils/tool-choice";
|
|
69
|
+
import {
|
|
70
|
+
AnthropicConnectionTimeoutError,
|
|
71
|
+
type AnthropicFetchOptions,
|
|
72
|
+
AnthropicMessagesClient,
|
|
73
|
+
type AnthropicMessagesClientLike,
|
|
74
|
+
calculateAnthropicRetryDelayMs,
|
|
75
|
+
} from "./anthropic-client";
|
|
76
|
+
import {
|
|
77
|
+
type ToolInputSchema as AnthropicToolInputSchema,
|
|
78
|
+
type Tool as AnthropicWireTool,
|
|
79
|
+
type Usage as AnthropicWireUsage,
|
|
80
|
+
type ContentBlockParam,
|
|
81
|
+
type FallbackParam,
|
|
82
|
+
isAnthropicWebSearchHistoryBlock,
|
|
83
|
+
type MessageCreateParamsStreaming,
|
|
84
|
+
type MessageParam,
|
|
85
|
+
type RawMessageStreamEvent,
|
|
86
|
+
type TextBlockParam,
|
|
87
|
+
} from "./anthropic-wire";
|
|
88
|
+
import {
|
|
89
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS,
|
|
90
|
+
claudeCodeSystemInstruction,
|
|
91
|
+
claudeCodeVersion,
|
|
92
|
+
claudeToolPrefix,
|
|
93
|
+
coworkUserAgent,
|
|
94
|
+
} from "./claude-code-fingerprint";
|
|
95
|
+
import {
|
|
96
|
+
buildCopilotDynamicHeaders,
|
|
97
|
+
hasCopilotVisionInput,
|
|
98
|
+
resolveGitHubCopilotBaseUrl,
|
|
99
|
+
} from "./github-copilot-headers";
|
|
100
|
+
import { getOpenAIPromptCacheKey } from "./openai-shared";
|
|
101
|
+
import { transformMessages } from "./transform-messages";
|
|
102
|
+
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
103
|
+
|
|
104
|
+
export type AnthropicHeaderOptions = {
|
|
105
|
+
apiKey: string;
|
|
106
|
+
baseUrl?: string;
|
|
107
|
+
isOAuth?: boolean;
|
|
108
|
+
extraBetas?: string[];
|
|
109
|
+
stream?: boolean;
|
|
110
|
+
modelHeaders?: Record<string, string>;
|
|
111
|
+
isCloudflareAiGateway?: boolean;
|
|
112
|
+
claudeCodeSessionId?: string;
|
|
113
|
+
coworkBetas?: readonly string[];
|
|
114
|
+
/** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */
|
|
115
|
+
allowAnthropicHeaderOverrides?: boolean;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
|
|
119
|
+
const trimmed = baseUrl?.trim();
|
|
120
|
+
if (!trimmed) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
|
|
124
|
+
return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build deduplicated beta header string
|
|
128
|
+
export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readonly string[]): string {
|
|
129
|
+
const seen = new Set<string>();
|
|
130
|
+
const result: string[] = [];
|
|
131
|
+
for (const beta of [...baseBetas, ...extraBetas]) {
|
|
132
|
+
const trimmed = beta.trim();
|
|
133
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
134
|
+
seen.add(trimmed);
|
|
135
|
+
result.push(trimmed);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result.join(",");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Merge an extra Anthropic beta into a caller-provided `anthropic-beta` header,
|
|
143
|
+
* preserving the caller's key casing and deduping the tokens. Returns a
|
|
144
|
+
* single-entry header record for a per-request `headers` override — used to
|
|
145
|
+
* attach a required beta to injected SDK clients that bypass the client-level
|
|
146
|
+
* beta construction.
|
|
147
|
+
*/
|
|
148
|
+
function mergeAnthropicBetaHeader(callerHeaders: Record<string, string>, beta: string): Record<string, string> {
|
|
149
|
+
for (const key in callerHeaders) {
|
|
150
|
+
if (key.toLowerCase() === "anthropic-beta") {
|
|
151
|
+
return { [key]: buildBetaHeader(normalizeExtraBetas(callerHeaders[key]), [beta]) };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { "anthropic-beta": beta };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const midConversationSystemBeta = "mid-conversation-system-2026-04-07";
|
|
158
|
+
const contextManagementBeta = "context-management-2025-06-27";
|
|
159
|
+
const structuredOutputsBeta = "structured-outputs-2025-12-15";
|
|
160
|
+
const thinkingTokenCountBeta = "thinking-token-count-2026-05-13";
|
|
161
|
+
const fallbackCreditBeta = "fallback-credit-2026-06-01";
|
|
162
|
+
const coworkUtilityBetaDefaults = [
|
|
163
|
+
"interleaved-thinking-2025-05-14",
|
|
164
|
+
thinkingTokenCountBeta,
|
|
165
|
+
contextManagementBeta,
|
|
166
|
+
"prompt-caching-scope-2026-01-05",
|
|
167
|
+
structuredOutputsBeta,
|
|
168
|
+
] as const;
|
|
169
|
+
const coworkAgentBetaDefaults = [
|
|
170
|
+
"claude-code-20250219",
|
|
171
|
+
"interleaved-thinking-2025-05-14",
|
|
172
|
+
thinkingTokenCountBeta,
|
|
173
|
+
contextManagementBeta,
|
|
174
|
+
"prompt-caching-scope-2026-01-05",
|
|
175
|
+
midConversationSystemBeta,
|
|
176
|
+
"advanced-tool-use-2025-11-20",
|
|
177
|
+
] as const;
|
|
178
|
+
const extendedCacheTtlBeta = "extended-cache-ttl-2025-04-11";
|
|
179
|
+
const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
|
|
180
|
+
const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
|
|
181
|
+
const fastModeBeta = "fast-mode-2026-02-01";
|
|
182
|
+
const taskBudgetBeta = "task-budgets-2026-03-13";
|
|
183
|
+
const effortBeta = "effort-2025-11-24";
|
|
184
|
+
const serverSideFallbackBeta = "server-side-fallback-2026-06-01";
|
|
185
|
+
|
|
186
|
+
function buildCoworkBetas(
|
|
187
|
+
agentRequest: boolean,
|
|
188
|
+
thinkingRequest: boolean,
|
|
189
|
+
disableStrictTools = false,
|
|
190
|
+
): readonly string[] {
|
|
191
|
+
// `context-1m-2025-08-07` is intentionally never advertised. OAuth
|
|
192
|
+
// subscription credentials have no long-context credit balance, so Anthropic
|
|
193
|
+
// hard-429s ("Usage credits are required for long context requests") on any
|
|
194
|
+
// beta-gated 1M model regardless of prompt size (#7238). Natively-1M models
|
|
195
|
+
// (e.g. claude-sonnet-5) serve their full window without the beta anyway.
|
|
196
|
+
if (!agentRequest && !disableStrictTools) return coworkUtilityBetaDefaults;
|
|
197
|
+
const betas: string[] = [];
|
|
198
|
+
for (const beta of agentRequest ? coworkAgentBetaDefaults : coworkUtilityBetaDefaults) {
|
|
199
|
+
if (disableStrictTools && beta === structuredOutputsBeta) continue;
|
|
200
|
+
betas.push(beta);
|
|
201
|
+
}
|
|
202
|
+
if (!agentRequest) return betas;
|
|
203
|
+
if (thinkingRequest) betas.push(effortBeta);
|
|
204
|
+
betas.push(fallbackCreditBeta);
|
|
205
|
+
return betas;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
|
|
209
|
+
if (!headers) return undefined;
|
|
210
|
+
const normalizedName = headerName.toLowerCase();
|
|
211
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
212
|
+
if (key.toLowerCase() === normalizedName) return value;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isClaudeCodeClientUserAgent(userAgent: string | undefined): userAgent is string {
|
|
218
|
+
if (!userAgent) return false;
|
|
219
|
+
return userAgent.toLowerCase().startsWith("claude-cli");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sharedHeaders = {
|
|
223
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
224
|
+
Connection: "keep-alive",
|
|
225
|
+
"Content-Type": "application/json",
|
|
226
|
+
"anthropic-version": "2023-06-01",
|
|
227
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
228
|
+
"x-app": "cli",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
|
|
232
|
+
const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
|
|
233
|
+
const extraBetas = options.extraBetas ?? [];
|
|
234
|
+
const stream = options.stream ?? false;
|
|
235
|
+
// `enforcedHeaderKeys` strips User-Agent / X-Api-Key / Authorization out of
|
|
236
|
+
// modelHeaders so a case-insensitive spread can't produce duplicate keys; each
|
|
237
|
+
// branch re-adds the caller's value explicitly. User-Agent and X-Api-Key are
|
|
238
|
+
// always honored (with branch-specific defaults filling in when absent), while
|
|
239
|
+
// Authorization is honored for every non-OAuth, non-Cloudflare-gateway branch —
|
|
240
|
+
// OAuth requests MUST carry `Authorization: Bearer <oauth-token>` (the OAuth
|
|
241
|
+
// credential itself) and Cloudflare AI Gateway authenticates via
|
|
242
|
+
// `cf-aig-authorization`, so user-supplied auth there would just leak. Both of
|
|
243
|
+
// those cases drop + log the caller value (#3391).
|
|
244
|
+
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
245
|
+
const incomingAuthorization = getHeaderCaseInsensitive(options.modelHeaders, "Authorization");
|
|
246
|
+
const incomingApiKey = getHeaderCaseInsensitive(options.modelHeaders, "X-Api-Key");
|
|
247
|
+
// Cowork's beta profile is part of the OAuth fingerprint; API-key requests
|
|
248
|
+
// default to extras only, matching the streaming path.
|
|
249
|
+
const betaHeader = buildBetaHeader(
|
|
250
|
+
options.coworkBetas ?? (oauthToken ? buildCoworkBetas(true, true) : []),
|
|
251
|
+
extraBetas,
|
|
252
|
+
);
|
|
253
|
+
const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
|
|
254
|
+
const isCloudflare = options.isCloudflareAiGateway ?? false;
|
|
255
|
+
const honorAuthorization = !oauthToken && !isCloudflare;
|
|
256
|
+
const allowAnthropicHeaderOverrides =
|
|
257
|
+
oauthToken &&
|
|
258
|
+
options.allowAnthropicHeaderOverrides === true &&
|
|
259
|
+
!isCloudflare &&
|
|
260
|
+
!isOfficialAnthropicApiUrl(options.baseUrl);
|
|
261
|
+
const honorApiKey = !isCloudflare;
|
|
262
|
+
const modelHeaders: Record<string, string> = {};
|
|
263
|
+
const anthropicHeaderOverrides: Record<string, string> = {};
|
|
264
|
+
const filteredEnforcedKeys: string[] = [];
|
|
265
|
+
const headerSource = options.modelHeaders;
|
|
266
|
+
if (headerSource) {
|
|
267
|
+
for (const key in headerSource) {
|
|
268
|
+
const value = headerSource[key];
|
|
269
|
+
const lowerKey = key.toLowerCase();
|
|
270
|
+
if (enforcedHeaderKeys.has(lowerKey)) {
|
|
271
|
+
if (allowAnthropicHeaderOverrides && overridableAnthropicHeaderKeys.has(lowerKey)) {
|
|
272
|
+
anthropicHeaderOverrides[key] = value;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// user-agent is always re-applied explicitly. authorization / x-api-key
|
|
276
|
+
// are silently re-applied in honoring branches and dropped + logged
|
|
277
|
+
// where the branch enforces its own credential.
|
|
278
|
+
if (lowerKey === "user-agent") continue;
|
|
279
|
+
if (lowerKey === "authorization" && honorAuthorization) continue;
|
|
280
|
+
if (lowerKey === "x-api-key" && honorApiKey) continue;
|
|
281
|
+
filteredEnforcedKeys.push(key);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
modelHeaders[key] = value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (filteredEnforcedKeys.length > 0) {
|
|
288
|
+
// Caller/env-supplied values (options.headers, ANTHROPIC_CUSTOM_HEADERS)
|
|
289
|
+
// for enforced headers are replaced by our own values; say so instead of
|
|
290
|
+
// dropping them silently. Keys only — values may carry credentials.
|
|
291
|
+
logger.debug("anthropic: ignoring caller-supplied enforced headers", {
|
|
292
|
+
headers: filteredEnforcedKeys,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isCloudflare) {
|
|
297
|
+
return {
|
|
298
|
+
...modelHeaders,
|
|
299
|
+
Accept: acceptHeader,
|
|
300
|
+
...sharedHeaders,
|
|
301
|
+
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
302
|
+
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
303
|
+
"cf-aig-authorization": `Bearer ${options.apiKey}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (oauthToken) {
|
|
308
|
+
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent) ? incomingUserAgent : coworkUserAgent;
|
|
309
|
+
const headers = {
|
|
310
|
+
...modelHeaders,
|
|
311
|
+
Accept: acceptHeader,
|
|
312
|
+
"Content-Type": "application/json",
|
|
313
|
+
"User-Agent": userAgent,
|
|
314
|
+
...(options.claudeCodeSessionId ? { "X-Claude-Code-Session-Id": options.claudeCodeSessionId } : {}),
|
|
315
|
+
...coworkHeaders,
|
|
316
|
+
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
317
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
318
|
+
"anthropic-version": "2023-06-01",
|
|
319
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
320
|
+
"x-app": "cli",
|
|
321
|
+
"x-client-request-id": nodeCrypto.randomUUID(),
|
|
322
|
+
Connection: "keep-alive",
|
|
323
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
324
|
+
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
325
|
+
};
|
|
326
|
+
return allowAnthropicHeaderOverrides ? mergeHeaders(headers, anthropicHeaderOverrides) : headers;
|
|
327
|
+
} else if (!isOfficialAnthropicApiUrl(options.baseUrl)) {
|
|
328
|
+
return {
|
|
329
|
+
...modelHeaders,
|
|
330
|
+
Accept: acceptHeader,
|
|
331
|
+
Authorization: incomingAuthorization ?? `Bearer ${options.apiKey}`,
|
|
332
|
+
...sharedHeaders,
|
|
333
|
+
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
334
|
+
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
335
|
+
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
336
|
+
};
|
|
337
|
+
} else {
|
|
338
|
+
return {
|
|
339
|
+
...modelHeaders,
|
|
340
|
+
Accept: acceptHeader,
|
|
341
|
+
...sharedHeaders,
|
|
342
|
+
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
343
|
+
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
344
|
+
...(incomingAuthorization ? { Authorization: incomingAuthorization } : {}),
|
|
345
|
+
"X-Api-Key": incomingApiKey ?? options.apiKey,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
type AnthropicCacheControl = NonNullable<TextBlockParam["cache_control"]>;
|
|
351
|
+
type AnthropicImageMediaType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
352
|
+
|
|
353
|
+
function normalizeAnthropicImageMediaType(mimeType: string): AnthropicImageMediaType | undefined {
|
|
354
|
+
const normalized = mimeType.trim().toLowerCase();
|
|
355
|
+
if (normalized === "image/jpg") return "image/jpeg";
|
|
356
|
+
if (
|
|
357
|
+
normalized === "image/jpeg" ||
|
|
358
|
+
normalized === "image/png" ||
|
|
359
|
+
normalized === "image/gif" ||
|
|
360
|
+
normalized === "image/webp"
|
|
361
|
+
) {
|
|
362
|
+
return normalized;
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function cloneAnthropicCacheControl(cacheControl: AnthropicCacheControl): AnthropicCacheControl {
|
|
368
|
+
return { ...cacheControl };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
type AnthropicOutputConfig = NonNullable<MessageCreateParamsStreaming["output_config"]>;
|
|
372
|
+
|
|
373
|
+
const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
|
|
374
|
+
let warnedStopSequencesTrim = false;
|
|
375
|
+
|
|
376
|
+
const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
|
|
377
|
+
|
|
378
|
+
type AnthropicProviderSessionState = ProviderSessionState & {
|
|
379
|
+
strictToolsDisabled: boolean;
|
|
380
|
+
fastModeDisabled: boolean;
|
|
381
|
+
/**
|
|
382
|
+
* Runtime-learned: this endpoint returned `400 Invalid signature in
|
|
383
|
+
* thinking block` for a replayed unsigned thinking block, so it must be
|
|
384
|
+
* treated as a signing proxy from now on. All subsequent requests demote
|
|
385
|
+
* unsigned thinking to text for this (baseUrl, modelId), same behavior as
|
|
386
|
+
* an explicit `compat.replayUnsignedThinking: false`. Cleared on session
|
|
387
|
+
* close.
|
|
388
|
+
*/
|
|
389
|
+
replayUnsignedThinkingDisabled: boolean;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
393
|
+
const state: AnthropicProviderSessionState = {
|
|
394
|
+
strictToolsDisabled: false,
|
|
395
|
+
fastModeDisabled: false,
|
|
396
|
+
replayUnsignedThinkingDisabled: false,
|
|
397
|
+
close: () => {
|
|
398
|
+
state.strictToolsDisabled = false;
|
|
399
|
+
state.fastModeDisabled = false;
|
|
400
|
+
state.replayUnsignedThinkingDisabled = false;
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
return state;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Key the sticky strict-tools / fast-mode learning per endpoint+model. A
|
|
408
|
+
* grammar-too-large 400 or a fast-mode rejection is specific to the model (its
|
|
409
|
+
* tool grammar / entitlement) and the endpoint (direct Anthropic vs a gateway /
|
|
410
|
+
* Foundry / Bedrock proxy), so it MUST NOT bleed onto unrelated anthropic-messages
|
|
411
|
+
* requests in the same session. NUL separates the two components so neither can
|
|
412
|
+
* forge the boundary.
|
|
413
|
+
*/
|
|
414
|
+
function anthropicProviderSessionStateKey(baseUrl: string, modelId: string): string {
|
|
415
|
+
return `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:${baseUrl}\u0000${modelId}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getAnthropicProviderSessionState(
|
|
419
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
420
|
+
baseUrl: string,
|
|
421
|
+
modelId: string,
|
|
422
|
+
): AnthropicProviderSessionState | undefined {
|
|
423
|
+
if (!providerSessionState) return undefined;
|
|
424
|
+
const key = anthropicProviderSessionStateKey(baseUrl, modelId);
|
|
425
|
+
const existing = providerSessionState.get(key) as AnthropicProviderSessionState | undefined;
|
|
426
|
+
if (existing) return existing;
|
|
427
|
+
const created = createAnthropicProviderSessionState();
|
|
428
|
+
providerSessionState.set(key, created);
|
|
429
|
+
return created;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Clears the in-session "server rejected fast mode" sticky flag. Call when the
|
|
434
|
+
* caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
|
|
435
|
+
* `/fast on` after a previous turn auto-disabled it) so the next request
|
|
436
|
+
* actually carries `speed: "fast"` again. No-op when the map or state entry
|
|
437
|
+
* hasn't been materialized yet.
|
|
438
|
+
*/
|
|
439
|
+
export function clearAnthropicFastModeFallback(
|
|
440
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
441
|
+
): void {
|
|
442
|
+
if (!providerSessionState) return;
|
|
443
|
+
// Fast mode is re-armed session-wide (user toggled `/fast on`), so clear the
|
|
444
|
+
// sticky flag on every per-endpoint/model Anthropic entry — plus the legacy
|
|
445
|
+
// unscoped key — rather than a single shared object.
|
|
446
|
+
const prefix = `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:`;
|
|
447
|
+
for (const [key, value] of providerSessionState) {
|
|
448
|
+
if (key !== ANTHROPIC_PROVIDER_SESSION_STATE_KEY && !key.startsWith(prefix)) continue;
|
|
449
|
+
(value as AnthropicProviderSessionState).fastModeDisabled = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Whether the direct Anthropic model's endpoint-scoped fast-mode fallback is
|
|
454
|
+
* currently active. Reading the map directly is intentional: inspection must
|
|
455
|
+
* not materialize a state entry for a model that has never streamed.
|
|
456
|
+
*/
|
|
457
|
+
export function isAnthropicFastModeFallbackDisabled(
|
|
458
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
459
|
+
model: Model<Api>,
|
|
460
|
+
): boolean {
|
|
461
|
+
if (!providerSessionState || model.provider !== "anthropic" || model.api !== "anthropic-messages") return false;
|
|
462
|
+
const baseUrl = resolveAnthropicBaseUrl(model as Model<"anthropic-messages">) ?? "https://api.anthropic.com";
|
|
463
|
+
const key = anthropicProviderSessionStateKey(baseUrl, model.id);
|
|
464
|
+
return (providerSessionState.get(key) as AnthropicProviderSessionState | undefined)?.fastModeDisabled ?? false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
|
|
468
|
+
return params.tools?.some(tool => tool.strict === true) ?? false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function dropAnthropicFastMode(params: MessageCreateParamsStreaming): void {
|
|
472
|
+
delete params.speed;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function dropAnthropicStrictTools(params: MessageCreateParamsStreaming): void {
|
|
476
|
+
if (!params.tools) return;
|
|
477
|
+
for (const tool of params.tools) {
|
|
478
|
+
delete tool.strict;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getCacheControl(
|
|
483
|
+
model: Model<"anthropic-messages">,
|
|
484
|
+
cacheRetention: CacheRetention | undefined,
|
|
485
|
+
isOAuthToken: boolean,
|
|
486
|
+
): { retention: CacheRetention; cacheControl?: AnthropicCacheControl } {
|
|
487
|
+
// OAuth mirrors Claude Code and always defaults to 1h retention. API-key
|
|
488
|
+
// requests also default to 1h where the endpoint supports it (canonical
|
|
489
|
+
// Anthropic API, `compat.supportsLongCacheRetention`): agent sessions
|
|
490
|
+
// routinely idle past 5 minutes waiting on background jobs, and a 5m
|
|
491
|
+
// breakpoint cold-misses the entire prefix on resume. PI_CACHE_RETENTION
|
|
492
|
+
// still overrides the API-key default in either direction.
|
|
493
|
+
const retention = isOAuthToken
|
|
494
|
+
? (cacheRetention ?? "long")
|
|
495
|
+
: resolveCacheRetention(cacheRetention, model.compat.supportsLongCacheRetention ? "long" : "short");
|
|
496
|
+
if (retention === "none") {
|
|
497
|
+
return { retention };
|
|
498
|
+
}
|
|
499
|
+
const ttl = retention === "long" && model.compat.supportsLongCacheRetention ? "1h" : undefined;
|
|
500
|
+
return {
|
|
501
|
+
retention,
|
|
502
|
+
cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Cowork mode: mimic the desktop agent's direct inference transport. Constants
|
|
507
|
+
// live in the leaf module so registry/usage consumers avoid an init cycle.
|
|
508
|
+
export * from "./claude-code-fingerprint";
|
|
509
|
+
|
|
510
|
+
export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
|
|
511
|
+
switch (arch.toLowerCase()) {
|
|
512
|
+
case "amd64":
|
|
513
|
+
case "x64":
|
|
514
|
+
return "x64";
|
|
515
|
+
case "arm64":
|
|
516
|
+
case "aarch64":
|
|
517
|
+
return "arm64";
|
|
518
|
+
case "386":
|
|
519
|
+
case "x86":
|
|
520
|
+
case "ia32":
|
|
521
|
+
return "x86";
|
|
522
|
+
default:
|
|
523
|
+
return `other::${arch.toLowerCase()}`;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Static headers emitted by Cowork's Linux Claude runtime. */
|
|
528
|
+
export const coworkHeaders = {
|
|
529
|
+
"X-Stainless-Arch": mapStainlessArch(process.arch),
|
|
530
|
+
"X-Stainless-Lang": "js",
|
|
531
|
+
"X-Stainless-OS": "Linux",
|
|
532
|
+
"X-Stainless-Package-Version": "0.94.0",
|
|
533
|
+
"X-Stainless-Retry-Count": "0",
|
|
534
|
+
"X-Stainless-Runtime": "node",
|
|
535
|
+
"X-Stainless-Runtime-Version": "v26.3.0",
|
|
536
|
+
"X-Stainless-Timeout": "600",
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const enforcedHeaderKeys = new Set(
|
|
540
|
+
[
|
|
541
|
+
...Object.keys(coworkHeaders),
|
|
542
|
+
"Accept",
|
|
543
|
+
"Accept-Encoding",
|
|
544
|
+
"Connection",
|
|
545
|
+
"Content-Type",
|
|
546
|
+
"anthropic-version",
|
|
547
|
+
"anthropic-dangerous-direct-browser-access",
|
|
548
|
+
"anthropic-beta",
|
|
549
|
+
"User-Agent",
|
|
550
|
+
"x-app",
|
|
551
|
+
"Authorization",
|
|
552
|
+
"X-Api-Key",
|
|
553
|
+
"X-Claude-Code-Session-Id",
|
|
554
|
+
"x-client-request-id",
|
|
555
|
+
"cf-aig-authorization",
|
|
556
|
+
].map(key => key.toLowerCase()),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const overridableAnthropicHeaderKeys = new Set(
|
|
560
|
+
[...Object.keys(coworkHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
564
|
+
|
|
565
|
+
function createClaudeBillingHeader(firstUserMessageText: string): string {
|
|
566
|
+
// Fingerprint: SHA256(salt + msg[4] + msg[7] + msg[20] + version)[:3]
|
|
567
|
+
// Matches CC's computeFingerprint in utils/fingerprint.ts.
|
|
568
|
+
// Uses chars from the first user message (not the system prompt).
|
|
569
|
+
const k = [4, 7, 20].map(i => firstUserMessageText[i] ?? "0").join("");
|
|
570
|
+
const versionSuffix = nodeCrypto
|
|
571
|
+
.createHash("sha256")
|
|
572
|
+
.update(`59cf53e54c78${k}${claudeCodeVersion}`)
|
|
573
|
+
.digest("hex")
|
|
574
|
+
.slice(0, 3);
|
|
575
|
+
// cch=00000: placeholder replaced with the real attestation hash by wrapFetchForCch
|
|
576
|
+
// before the request hits the wire (see below).
|
|
577
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${versionSuffix}; cc_entrypoint=claude-desktop; ${CCH_PLACEHOLDER_STR};`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// cch attestation: XXHash64(body_with_placeholder, seed) low-20-bits, 5 hex chars.
|
|
581
|
+
const CCH_SEED = 0x4d659218e32a3268n;
|
|
582
|
+
const CCH_PLACEHOLDER_STR = "cch=00000";
|
|
583
|
+
const cchEncoder = new TextEncoder();
|
|
584
|
+
const CCH_PLACEHOLDER = cchEncoder.encode(CCH_PLACEHOLDER_STR);
|
|
585
|
+
// Combined anchor for the billing-header placeholder inside system[0].
|
|
586
|
+
// "system":[{"type":"text","text":"x-anthropic-billing-header:
|
|
587
|
+
// Matches the exact JSON prefix of the first system block when
|
|
588
|
+
// createClaudeBillingHeader injects system[0]. "messages" serializes before
|
|
589
|
+
// "system" in Anthropic SDK payloads (~byte 29 vs ~byte 4705), so user content
|
|
590
|
+
// in the messages array can never match this sequence. User system prompt text
|
|
591
|
+
// lives in system[2] and therefore also cannot match.
|
|
592
|
+
const BILLING_SYSTEM_MARKER = cchEncoder.encode(`"system":[{"type":"text","text":"${CLAUDE_BILLING_HEADER_PREFIX}`);
|
|
593
|
+
const CCH_BILLING_SEARCH_WINDOW = 150;
|
|
594
|
+
|
|
595
|
+
function patchCch(body: Uint8Array): "patched" | "no-billing-header" | "unanchored" {
|
|
596
|
+
// Zero-copy Buffer view over the same memory; its `indexOf` is a native memmem,
|
|
597
|
+
// ~7.5x faster than a hand-rolled byte loop here — the marker sits ~99% through
|
|
598
|
+
// the body because `messages` serializes before `system`, so a JS scan would
|
|
599
|
+
// walk almost the entire payload (benchmarked: 563µs -> 75µs on a 1MB body).
|
|
600
|
+
const view = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
601
|
+
|
|
602
|
+
// Find the combined system[0] + billing-header prefix marker.
|
|
603
|
+
const markerIdx = view.indexOf(BILLING_SYSTEM_MARKER);
|
|
604
|
+
if (markerIdx === -1) return "no-billing-header"; // no CC billing header injected
|
|
605
|
+
|
|
606
|
+
// Placeholder must sit within CCH_BILLING_SEARCH_WINDOW bytes after the marker.
|
|
607
|
+
const searchFrom = markerIdx + BILLING_SYSTEM_MARKER.length;
|
|
608
|
+
const idx = view.indexOf(CCH_PLACEHOLDER, searchFrom);
|
|
609
|
+
if (idx === -1 || idx - searchFrom > CCH_BILLING_SEARCH_WINDOW) return "unanchored";
|
|
610
|
+
|
|
611
|
+
// Hash the body with the placeholder in place (matches CC's in-place behaviour).
|
|
612
|
+
const h = Bun.hash.xxHash64(body, CCH_SEED);
|
|
613
|
+
const cch = (h & 0xfffffn).toString(16).padStart(5, "0");
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < 5; i++) body[idx + 4 + i] = cch.charCodeAt(i);
|
|
616
|
+
return "patched";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Wraps a fetch implementation to patch the Claude Code billing-header `cch`
|
|
621
|
+
* attestation into outgoing request bodies. Bodies without the placeholder
|
|
622
|
+
* pass through untouched, so installing it on every OAuth flow is safe.
|
|
623
|
+
*/
|
|
624
|
+
export function wrapFetchForCch(base: FetchImpl): FetchImpl {
|
|
625
|
+
return (input, init) => {
|
|
626
|
+
if (init?.body && typeof init.body === "string" && init.body.includes(CCH_PLACEHOLDER_STR)) {
|
|
627
|
+
const encoded = cchEncoder.encode(init.body);
|
|
628
|
+
if (patchCch(encoded) === "unanchored") {
|
|
629
|
+
// The OAuth billing placeholder is anchored to system[0] but we couldn't
|
|
630
|
+
// patch it — e.g. an `onPayload` hook reordered the first system block's keys
|
|
631
|
+
// so BILLING_SYSTEM_MARKER no longer matches. Send the body as-is (cch stays
|
|
632
|
+
// `00000`, the prior behaviour) rather than failing the request, but surface the
|
|
633
|
+
// fingerprint regression instead of letting it ship silently. A `cch=00000`
|
|
634
|
+
// literal in user content alone ("no-billing-header") is not a regression.
|
|
635
|
+
logger.warn("anthropic: cch billing placeholder present but not patched; sending unattested request");
|
|
636
|
+
}
|
|
637
|
+
return base(input, { ...init, body: encoded });
|
|
638
|
+
}
|
|
639
|
+
return base(input, init);
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
644
|
+
/^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
645
|
+
|
|
646
|
+
export function isClaudeCloakingUserId(userId: string): boolean {
|
|
647
|
+
return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Real Claude Code sends `metadata.user_id` as a JSON-stringified object of the
|
|
652
|
+
* shape `{ device_id, account_uuid, session_id, ...extra }` (see
|
|
653
|
+
* services/api/claude.ts → getAPIMetadata). Accept that shape so callers that
|
|
654
|
+
* supply a stable `session_id` aren't silently overwritten with fresh entropy
|
|
655
|
+
* on every request, which would inflate the backend session count.
|
|
656
|
+
*/
|
|
657
|
+
function isClaudeJsonUserId(userId: string): boolean {
|
|
658
|
+
if (userId.length === 0 || userId[0] !== "{") return false;
|
|
659
|
+
let parsed: unknown;
|
|
660
|
+
try {
|
|
661
|
+
parsed = JSON.parse(userId);
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
666
|
+
const obj = parsed as Record<string, unknown>;
|
|
667
|
+
return typeof obj.session_id === "string" && obj.session_id.length > 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function extractClaudeMetadataSessionId(userId: unknown): string | undefined {
|
|
671
|
+
if (typeof userId !== "string") return undefined;
|
|
672
|
+
if (isClaudeCloakingUserId(userId)) {
|
|
673
|
+
return userId.slice(userId.lastIndexOf("_session_") + "_session_".length);
|
|
674
|
+
}
|
|
675
|
+
if (userId.length === 0 || userId[0] !== "{") return undefined;
|
|
676
|
+
let parsed: unknown;
|
|
677
|
+
try {
|
|
678
|
+
parsed = JSON.parse(userId);
|
|
679
|
+
} catch {
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
683
|
+
const sessionId = (parsed as Record<string, unknown>).session_id;
|
|
684
|
+
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function generateClaudeCloakingUserId(): string {
|
|
688
|
+
const userHash = nodeCrypto.randomBytes(32).toString("hex");
|
|
689
|
+
const accountId = nodeCrypto.randomUUID().toLowerCase();
|
|
690
|
+
const sessionId = nodeCrypto.randomUUID().toLowerCase();
|
|
691
|
+
return `user_${userHash}_account_${accountId}_session_${sessionId}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN = "omp-claude-device-id-v1:";
|
|
695
|
+
const CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN = "omp-claude-device-id-v2";
|
|
696
|
+
|
|
697
|
+
export function deriveClaudeDeviceId(installId: string, accountId?: string): string {
|
|
698
|
+
const hash = nodeCrypto.createHash("sha256");
|
|
699
|
+
if (accountId && accountId.length > 0) {
|
|
700
|
+
return hash
|
|
701
|
+
.update(CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN)
|
|
702
|
+
.update("\0")
|
|
703
|
+
.update(installId)
|
|
704
|
+
.update("\0")
|
|
705
|
+
.update(accountId)
|
|
706
|
+
.digest("hex");
|
|
707
|
+
}
|
|
708
|
+
return hash.update(CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN).update(installId).digest("hex");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function readMetadataString(metadata: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
712
|
+
const value = metadata?.[key];
|
|
713
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function readAnthropicMetadataAccountId(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
717
|
+
return (
|
|
718
|
+
readMetadataString(metadata, "account_uuid") ??
|
|
719
|
+
readMetadataString(metadata, "accountId") ??
|
|
720
|
+
readMetadataString(metadata, "account_id")
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function deriveClaudeDeviceIdFromInstallId(accountId?: string): string {
|
|
725
|
+
return deriveClaudeDeviceId(getInstallId(), accountId);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function generateClaudeJsonUserId(sessionId?: string, accountId?: string): string {
|
|
729
|
+
const userId: Record<string, string> = {
|
|
730
|
+
device_id: deriveClaudeDeviceIdFromInstallId(accountId),
|
|
731
|
+
session_id: sessionId ?? nodeCrypto.randomUUID().toLowerCase(),
|
|
732
|
+
};
|
|
733
|
+
if (accountId && accountId.length > 0) userId.account_uuid = accountId;
|
|
734
|
+
return JSON.stringify(userId);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Resolve the `metadata.user_id` field for an Anthropic Messages request.
|
|
739
|
+
*
|
|
740
|
+
* For API-key tokens, an explicit caller-supplied `userId` is forwarded
|
|
741
|
+
* verbatim and `undefined` yields no metadata. For OAuth tokens the value
|
|
742
|
+
* must match the Claude Code attribution shape (`isClaudeCloakingUserId` or
|
|
743
|
+
* the `{session_id, account_uuid?, device_id?}` JSON envelope) — anything
|
|
744
|
+
* else is dropped and a fresh Claude-Code-style JSON id is generated from
|
|
745
|
+
* `sessionId`/`accountId` so attribution stays consistent across the main
|
|
746
|
+
* streaming path and provider-specific request builders (e.g. web search).
|
|
747
|
+
*/
|
|
748
|
+
export function resolveAnthropicMetadataUserId(
|
|
749
|
+
userId: unknown,
|
|
750
|
+
isOAuthToken: boolean,
|
|
751
|
+
sessionId?: string,
|
|
752
|
+
accountId?: string,
|
|
753
|
+
): string | undefined {
|
|
754
|
+
if (typeof userId === "string") {
|
|
755
|
+
if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
|
|
756
|
+
return userId;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (!isOAuthToken) return undefined;
|
|
761
|
+
return generateClaudeJsonUserId(sessionId, accountId);
|
|
762
|
+
}
|
|
763
|
+
const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
|
|
764
|
+
const UMANS_WEBSEARCH_PROVIDER_HEADER = "X-Umans-Websearch-Provider";
|
|
765
|
+
const UMANS_WEBSEARCH_TOOL_NAME = "web_search";
|
|
766
|
+
export const applyClaudeToolPrefix = (name: string): string => {
|
|
767
|
+
if (!claudeToolPrefix) return name;
|
|
768
|
+
if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
|
|
769
|
+
// Always prepend (no "already prefixed" short-circuit): the prefix is a wire
|
|
770
|
+
// transport detail applied once to internal tool names, and `stripClaudeToolPrefix`
|
|
771
|
+
// removes exactly one prefix on receive. Skipping names that already start with the
|
|
772
|
+
// prefix would make a tool literally named `_foo` lose its leading underscore on the
|
|
773
|
+
// return trip (`_foo` → wire `_foo` → strip → `foo`), so the agent loop can't find it.
|
|
774
|
+
return `${claudeToolPrefix}${name}`;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
export const stripClaudeToolPrefix = (name: string): string => {
|
|
778
|
+
if (!claudeToolPrefix) return name;
|
|
779
|
+
if (!name.toLowerCase().startsWith(claudeToolPrefix.toLowerCase())) return name;
|
|
780
|
+
return name.slice(claudeToolPrefix.length);
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
function normalizeUmansWebSearchProvider(value: string | undefined): "native" | "exa" | undefined {
|
|
784
|
+
const normalized = value?.trim().toLowerCase();
|
|
785
|
+
return normalized === "native" || normalized === "exa" ? normalized : undefined;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function getUmansWebSearchProvider(headers: Record<string, string> | undefined): "native" | "exa" | undefined {
|
|
789
|
+
const explicit = getHeaderCaseInsensitive(headers, UMANS_WEBSEARCH_PROVIDER_HEADER);
|
|
790
|
+
if (explicit !== undefined) return normalizeUmansWebSearchProvider(explicit);
|
|
791
|
+
return normalizeUmansWebSearchProvider($env.UMANS_WEBSEARCH_PROVIDER);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function isUmansAnthropicModel(model: Model<"anthropic-messages">): boolean {
|
|
795
|
+
return model.provider === "umans" || model.baseUrl.toLowerCase().includes("api.code.umans.ai");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function getUmansWebSearchHeader(
|
|
799
|
+
model: Model<"anthropic-messages">,
|
|
800
|
+
headers: Record<string, string> | undefined,
|
|
801
|
+
): Record<string, string> | undefined {
|
|
802
|
+
if (!isUmansAnthropicModel(model)) return undefined;
|
|
803
|
+
const provider = getUmansWebSearchProvider(headers);
|
|
804
|
+
return provider ? { [UMANS_WEBSEARCH_PROVIDER_HEADER]: provider } : undefined;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function shouldUseUmansGatewayWebSearch(name: string, enabled: boolean): boolean {
|
|
808
|
+
return enabled && name.toLowerCase() === UMANS_WEBSEARCH_TOOL_NAME;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function encodeAnthropicToolName(
|
|
812
|
+
name: string,
|
|
813
|
+
isOAuthToken: boolean,
|
|
814
|
+
escapeBuiltinToolNames: boolean,
|
|
815
|
+
useUmansGatewayWebSearch = false,
|
|
816
|
+
): string {
|
|
817
|
+
if (shouldUseUmansGatewayWebSearch(name, useUmansGatewayWebSearch)) return name;
|
|
818
|
+
if (escapeBuiltinToolNames) return `${claudeToolPrefix}${name}`;
|
|
819
|
+
return isOAuthToken ? applyClaudeToolPrefix(name) : name;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function decodeAnthropicToolName(name: string, isOAuthToken: boolean, escapeBuiltinToolNames: boolean): string {
|
|
823
|
+
if (isOAuthToken || escapeBuiltinToolNames) return stripClaudeToolPrefix(name);
|
|
824
|
+
return name;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const ANTHROPIC_MANY_IMAGE_THRESHOLD = 20;
|
|
828
|
+
const ANTHROPIC_MANY_IMAGE_MAX_DIMENSION = 2000;
|
|
829
|
+
|
|
830
|
+
function countAnthropicImageBlocks(messages: Message[]): number {
|
|
831
|
+
let count = 0;
|
|
832
|
+
for (const message of messages) {
|
|
833
|
+
if (message.role !== "user" && message.role !== "developer" && message.role !== "toolResult") continue;
|
|
834
|
+
if (!Array.isArray(message.content)) continue;
|
|
835
|
+
for (const block of message.content) {
|
|
836
|
+
if (block.type === "image") count++;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return count;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const ANTHROPIC_IMAGE_RESIZE_CONCURRENCY = 4;
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Memoized resize results keyed on ImageContent identity. Callers keep message
|
|
846
|
+
* objects stable across turns, so without this every request (and every
|
|
847
|
+
* in-provider retry of a fresh turn) re-decodes and re-encodes the same
|
|
848
|
+
* oversized screenshots. A cached value identical to the key means "already
|
|
849
|
+
* within bounds / unresizable — skip the decode".
|
|
850
|
+
*/
|
|
851
|
+
const anthropicManyImageResizeCache = new WeakMap<ImageContent, ImageContent>();
|
|
852
|
+
|
|
853
|
+
type ResizeLimiter = <R>(fn: () => Promise<R>) => Promise<R>;
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Bounded-concurrency gate for image decode/encode work. The many-image path
|
|
857
|
+
* fans out over every block of every message; unbounded, 100+ oversized images
|
|
858
|
+
* would decode concurrently (two encode pipelines each) and spike memory by
|
|
859
|
+
* gigabytes. Slots are handed off directly to the next waiter on release.
|
|
860
|
+
*/
|
|
861
|
+
function createResizeLimiter(limit: number): ResizeLimiter {
|
|
862
|
+
let active = 0;
|
|
863
|
+
const queue: (() => void)[] = [];
|
|
864
|
+
return async fn => {
|
|
865
|
+
if (active >= limit) {
|
|
866
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
867
|
+
queue.push(resolve);
|
|
868
|
+
await promise;
|
|
869
|
+
} else {
|
|
870
|
+
active++;
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
return await fn();
|
|
874
|
+
} finally {
|
|
875
|
+
const next = queue.shift();
|
|
876
|
+
if (next) next();
|
|
877
|
+
else active--;
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async function resizeAnthropicManyImageBlock(block: ImageContent): Promise<ImageContent> {
|
|
883
|
+
try {
|
|
884
|
+
const inputBuffer = Buffer.from(block.data, "base64");
|
|
885
|
+
const { width, height } = await new Bun.Image(inputBuffer).metadata();
|
|
886
|
+
if (!width || !height) return block;
|
|
887
|
+
if (width <= ANTHROPIC_MANY_IMAGE_MAX_DIMENSION && height <= ANTHROPIC_MANY_IMAGE_MAX_DIMENSION) return block;
|
|
888
|
+
|
|
889
|
+
const scale = Math.min(ANTHROPIC_MANY_IMAGE_MAX_DIMENSION / width, ANTHROPIC_MANY_IMAGE_MAX_DIMENSION / height);
|
|
890
|
+
const targetWidth = Math.max(1, Math.min(ANTHROPIC_MANY_IMAGE_MAX_DIMENSION, Math.round(width * scale)));
|
|
891
|
+
const targetHeight = Math.max(1, Math.min(ANTHROPIC_MANY_IMAGE_MAX_DIMENSION, Math.round(height * scale)));
|
|
892
|
+
|
|
893
|
+
const [png, jpeg] = await Promise.all([
|
|
894
|
+
new Bun.Image(inputBuffer).resize(targetWidth, targetHeight).png().bytes(),
|
|
895
|
+
new Bun.Image(inputBuffer).resize(targetWidth, targetHeight).jpeg({ quality: 85 }).bytes(),
|
|
896
|
+
]);
|
|
897
|
+
const best =
|
|
898
|
+
png.length <= jpeg.length ? { buffer: png, mimeType: "image/png" } : { buffer: jpeg, mimeType: "image/jpeg" };
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
type: "image",
|
|
902
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
903
|
+
mimeType: best.mimeType,
|
|
904
|
+
};
|
|
905
|
+
} catch (error) {
|
|
906
|
+
logger.warn("anthropic: failed to resize oversized image for many-image request", {
|
|
907
|
+
mimeType: block.mimeType,
|
|
908
|
+
error: error instanceof Error ? error.message : String(error),
|
|
909
|
+
});
|
|
910
|
+
return block;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function resizeAnthropicManyImageContent(
|
|
915
|
+
content: (TextContent | ImageContent)[],
|
|
916
|
+
state: { resized: number },
|
|
917
|
+
limit: ResizeLimiter,
|
|
918
|
+
): Promise<(TextContent | ImageContent)[]> {
|
|
919
|
+
let changed = false;
|
|
920
|
+
const next = await Promise.all(
|
|
921
|
+
content.map(async block => {
|
|
922
|
+
if (block.type !== "image") return block;
|
|
923
|
+
let resized = anthropicManyImageResizeCache.get(block);
|
|
924
|
+
if (resized === undefined) {
|
|
925
|
+
resized = await limit(() => resizeAnthropicManyImageBlock(block));
|
|
926
|
+
anthropicManyImageResizeCache.set(block, resized);
|
|
927
|
+
}
|
|
928
|
+
if (resized !== block) {
|
|
929
|
+
changed = true;
|
|
930
|
+
state.resized++;
|
|
931
|
+
}
|
|
932
|
+
return resized;
|
|
933
|
+
}),
|
|
934
|
+
);
|
|
935
|
+
return changed ? next : content;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function resizeAnthropicManyImageMessage(
|
|
939
|
+
message: Message,
|
|
940
|
+
state: { resized: number },
|
|
941
|
+
limit: ResizeLimiter,
|
|
942
|
+
): Promise<Message> {
|
|
943
|
+
if (message.role === "user" || message.role === "developer") {
|
|
944
|
+
if (!Array.isArray(message.content)) return message;
|
|
945
|
+
const content = await resizeAnthropicManyImageContent(message.content, state, limit);
|
|
946
|
+
return content === message.content ? message : { ...message, content };
|
|
947
|
+
}
|
|
948
|
+
if (message.role === "toolResult") {
|
|
949
|
+
const content = await resizeAnthropicManyImageContent(message.content, state, limit);
|
|
950
|
+
return content === message.content ? message : { ...message, content };
|
|
951
|
+
}
|
|
952
|
+
return message;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function prepareAnthropicManyImageContext(context: Context, supportsImages: boolean): Promise<Context> {
|
|
956
|
+
if (!supportsImages) return context;
|
|
957
|
+
const imageCount = countAnthropicImageBlocks(context.messages);
|
|
958
|
+
if (imageCount <= ANTHROPIC_MANY_IMAGE_THRESHOLD) return context;
|
|
959
|
+
|
|
960
|
+
let changed = false;
|
|
961
|
+
const state = { resized: 0 };
|
|
962
|
+
const limit = createResizeLimiter(ANTHROPIC_IMAGE_RESIZE_CONCURRENCY);
|
|
963
|
+
const messages = await Promise.all(
|
|
964
|
+
context.messages.map(async message => {
|
|
965
|
+
const next = await resizeAnthropicManyImageMessage(message, state, limit);
|
|
966
|
+
if (next !== message) changed = true;
|
|
967
|
+
return next;
|
|
968
|
+
}),
|
|
969
|
+
);
|
|
970
|
+
if (!changed) return context;
|
|
971
|
+
logger.debug("anthropic: resized oversized images for many-image request", {
|
|
972
|
+
imageCount,
|
|
973
|
+
resized: state.resized,
|
|
974
|
+
maxDimension: ANTHROPIC_MANY_IMAGE_MAX_DIMENSION,
|
|
975
|
+
});
|
|
976
|
+
return { ...context, messages };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
type AnthropicToolResultContent =
|
|
980
|
+
| string
|
|
981
|
+
| Array<
|
|
982
|
+
| { type: "text"; text: string }
|
|
983
|
+
| {
|
|
984
|
+
type: "image";
|
|
985
|
+
source: {
|
|
986
|
+
type: "base64";
|
|
987
|
+
media_type: AnthropicImageMediaType;
|
|
988
|
+
data: string;
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
>;
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Convert content blocks to Anthropic API format
|
|
995
|
+
*/
|
|
996
|
+
function convertContentBlocks(
|
|
997
|
+
content: (TextContent | ImageContent)[],
|
|
998
|
+
supportsImages = true,
|
|
999
|
+
): AnthropicToolResultContent {
|
|
1000
|
+
const blocks: Array<
|
|
1001
|
+
| { type: "text"; text: string }
|
|
1002
|
+
| {
|
|
1003
|
+
type: "image";
|
|
1004
|
+
source: {
|
|
1005
|
+
type: "base64";
|
|
1006
|
+
media_type: AnthropicImageMediaType;
|
|
1007
|
+
data: string;
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
> = [];
|
|
1011
|
+
let sawText = false;
|
|
1012
|
+
let sawImage = false;
|
|
1013
|
+
|
|
1014
|
+
for (const block of content) {
|
|
1015
|
+
if (block.type === "text") {
|
|
1016
|
+
const text = block.text.toWellFormed();
|
|
1017
|
+
if (text.trim().length === 0) continue;
|
|
1018
|
+
sawText = true;
|
|
1019
|
+
blocks.push({ type: "text", text });
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (!supportsImages) {
|
|
1024
|
+
blocks.push({ type: "text", text: NON_VISION_IMAGE_PLACEHOLDER });
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const mediaType = normalizeAnthropicImageMediaType(block.mimeType);
|
|
1029
|
+
if (!mediaType) {
|
|
1030
|
+
blocks.push({ type: "text", text: `[unsupported image: ${block.mimeType}]` });
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
sawImage = true;
|
|
1035
|
+
blocks.push({
|
|
1036
|
+
type: "image",
|
|
1037
|
+
source: {
|
|
1038
|
+
type: "base64",
|
|
1039
|
+
media_type: mediaType,
|
|
1040
|
+
data: block.data,
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (!supportsImages) {
|
|
1046
|
+
return blocks
|
|
1047
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
1048
|
+
.map(block => block.text)
|
|
1049
|
+
.join("\n")
|
|
1050
|
+
.toWellFormed();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (sawImage && !sawText) {
|
|
1054
|
+
blocks.unshift({
|
|
1055
|
+
type: "text",
|
|
1056
|
+
text: "(see attached image)",
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return blocks;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
export type AnthropicOutputEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
1064
|
+
export type AnthropicEffort = AnthropicOutputEffort | "adaptive";
|
|
1065
|
+
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
1066
|
+
|
|
1067
|
+
export interface AnthropicOptions extends StreamOptions {
|
|
1068
|
+
/**
|
|
1069
|
+
* Enable extended thinking.
|
|
1070
|
+
* For adaptive-capable models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5):
|
|
1071
|
+
* uses adaptive thinking (Claude decides when/how much to think). For older
|
|
1072
|
+
* models: uses budget-based thinking with thinkingBudgetTokens.
|
|
1073
|
+
*/
|
|
1074
|
+
thinkingEnabled?: boolean;
|
|
1075
|
+
/**
|
|
1076
|
+
* Token budget for extended thinking (older models only).
|
|
1077
|
+
* Ignored for adaptive-capable models.
|
|
1078
|
+
*/
|
|
1079
|
+
thinkingBudgetTokens?: number;
|
|
1080
|
+
/**
|
|
1081
|
+
* Upstream wire model id override for collapsed effort-tier variants.
|
|
1082
|
+
* Serialized as `requestModelId ?? model.requestModelId ?? model.id`.
|
|
1083
|
+
*/
|
|
1084
|
+
requestModelId?: string;
|
|
1085
|
+
/**
|
|
1086
|
+
* Effort level for adaptive thinking.
|
|
1087
|
+
* Controls how much Claude allocates, or uses "adaptive" for MiniMax's
|
|
1088
|
+
* binary adaptive-thinking tag:
|
|
1089
|
+
* - "max": Always thinks with no constraints
|
|
1090
|
+
* - "high": Always thinks, deep reasoning (default)
|
|
1091
|
+
* - "medium": Moderate thinking, may skip for simple queries
|
|
1092
|
+
* - "low": Minimal thinking, skips for simple tasks
|
|
1093
|
+
* - "adaptive": Sends `thinking.type: "adaptive"` without `output_config.effort`
|
|
1094
|
+
* Ignored for older models.
|
|
1095
|
+
*/
|
|
1096
|
+
effort?: AnthropicEffort;
|
|
1097
|
+
/**
|
|
1098
|
+
* Optional reasoning level fallback for direct Anthropic provider usage.
|
|
1099
|
+
* Converted to adaptive effort when effort is not explicitly provided.
|
|
1100
|
+
*/
|
|
1101
|
+
reasoning?: SimpleStreamOptions["reasoning"];
|
|
1102
|
+
/**
|
|
1103
|
+
* Controls how Anthropic returns thinking content when the selected thinking
|
|
1104
|
+
* transport supports a display option. Defaults to "summarized" where the
|
|
1105
|
+
* API accepts it.
|
|
1106
|
+
*/
|
|
1107
|
+
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
1108
|
+
interleavedThinking?: boolean;
|
|
1109
|
+
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
|
1110
|
+
betas?: string[] | string;
|
|
1111
|
+
/**
|
|
1112
|
+
* Realization of `serviceTier: "priority"` on Anthropic models. When
|
|
1113
|
+
* `"priority"`, sets `speed: "fast"` on the request and appends the
|
|
1114
|
+
* `fast-mode-2026-02-01` beta header. Anthropic rejects unsupported models
|
|
1115
|
+
* with `invalid_request_error`, which triggers an in-provider one-shot
|
|
1116
|
+
* fallback (see `fastModeDisabled` provider state).
|
|
1117
|
+
*
|
|
1118
|
+
* Other `ServiceTier` values are currently ignored on this provider.
|
|
1119
|
+
*/
|
|
1120
|
+
serviceTier?: ServiceTier;
|
|
1121
|
+
/** Force OAuth bearer auth mode for proxy tokens that don't match Anthropic token prefixes. */
|
|
1122
|
+
isOAuth?: boolean;
|
|
1123
|
+
/**
|
|
1124
|
+
* Pre-built Anthropic Messages client. When provided, skips internal client
|
|
1125
|
+
* construction entirely. Accepts any structurally compatible client,
|
|
1126
|
+
* including SDK clients such as `AnthropicVertex`.
|
|
1127
|
+
*/
|
|
1128
|
+
client?: AnthropicMessagesClientLike;
|
|
1129
|
+
/**
|
|
1130
|
+
* Server-side fallback beta chain (`server-side-fallback-2026-06-01`).
|
|
1131
|
+
* When set, `fallbacks` is forwarded on the request body and the beta
|
|
1132
|
+
* header is auto-attached; the response parser then honors mid-stream
|
|
1133
|
+
* `fallback` content blocks and `usage.iterations` for served-model
|
|
1134
|
+
* promotion and per-attempt pricing. Opt-in ONLY — leaving this
|
|
1135
|
+
* undefined preserves the pre-fallback behavior on every code path.
|
|
1136
|
+
*/
|
|
1137
|
+
fallbacks?: FallbackParam[];
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export type AnthropicClientOptionsArgs = {
|
|
1141
|
+
model: Model<"anthropic-messages">;
|
|
1142
|
+
apiKey: string;
|
|
1143
|
+
extraBetas?: string[];
|
|
1144
|
+
stream?: boolean;
|
|
1145
|
+
interleavedThinking?: boolean;
|
|
1146
|
+
headers?: Record<string, string>;
|
|
1147
|
+
dynamicHeaders?: Record<string, string>;
|
|
1148
|
+
isOAuth?: boolean;
|
|
1149
|
+
hasTools?: boolean;
|
|
1150
|
+
thinkingEnabled?: boolean;
|
|
1151
|
+
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
1152
|
+
disableStrictTools?: boolean;
|
|
1153
|
+
fetch?: FetchImpl;
|
|
1154
|
+
maxRetryDelayMs?: number;
|
|
1155
|
+
claudeCodeSessionId?: string;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
export type AnthropicClientOptionsResult = {
|
|
1159
|
+
isOAuthToken: boolean;
|
|
1160
|
+
apiKey: string | null;
|
|
1161
|
+
authToken?: string | null;
|
|
1162
|
+
baseURL?: string;
|
|
1163
|
+
maxRetries: number;
|
|
1164
|
+
maxRetryDelayMs?: number;
|
|
1165
|
+
defaultHeaders: Record<string, string>;
|
|
1166
|
+
fetch?: FetchImpl;
|
|
1167
|
+
fetchOptions?: AnthropicFetchOptions;
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
const COWORK_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
|
|
1171
|
+
|
|
1172
|
+
type FoundryTlsOptions = {
|
|
1173
|
+
ca?: string | string[];
|
|
1174
|
+
cert?: string;
|
|
1175
|
+
key?: string;
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const foundryTlsOptionsCache = new Map<string, FoundryTlsOptions | undefined>();
|
|
1179
|
+
|
|
1180
|
+
function foundryTlsCacheKeyComponent(value: string | undefined): string | null {
|
|
1181
|
+
if (!value) return null;
|
|
1182
|
+
const trimmed = value.trim();
|
|
1183
|
+
// For path-valued vars, fold the file mtime into the key so on-disk cert
|
|
1184
|
+
// rotation (common for short-lived corporate mTLS certs) invalidates the
|
|
1185
|
+
// cached TLS options instead of pinning the first read forever.
|
|
1186
|
+
if (trimmed && !trimmed.includes("-----BEGIN") && looksLikeFilePath(trimmed)) {
|
|
1187
|
+
try {
|
|
1188
|
+
return `${trimmed}@${fs.statSync(trimmed).mtimeMs}`;
|
|
1189
|
+
} catch {
|
|
1190
|
+
return trimmed;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return value;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function foundryTlsOptionsCacheKey(): string {
|
|
1197
|
+
return JSON.stringify([
|
|
1198
|
+
foundryTlsCacheKeyComponent($env.NODE_EXTRA_CA_CERTS),
|
|
1199
|
+
foundryTlsCacheKeyComponent($env.CLAUDE_CODE_CLIENT_CERT),
|
|
1200
|
+
foundryTlsCacheKeyComponent($env.CLAUDE_CODE_CLIENT_KEY),
|
|
1201
|
+
]);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
|
|
1205
|
+
if (model.provider === "github-copilot") {
|
|
1206
|
+
return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
|
|
1207
|
+
}
|
|
1208
|
+
if (model.provider === "anthropic" && isFoundryEnabled()) {
|
|
1209
|
+
const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
|
|
1210
|
+
if (foundryBaseUrl) {
|
|
1211
|
+
return foundryBaseUrl;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (model.provider === "anthropic") {
|
|
1215
|
+
const configured = normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1216
|
+
// An explicitly configured non-official baseUrl (e.g. a models.yml provider
|
|
1217
|
+
// override) is more specific than the generic env fallback and wins.
|
|
1218
|
+
if (configured && !isOfficialAnthropicApiUrl(configured)) return configured;
|
|
1219
|
+
// Otherwise ANTHROPIC_BASE_URL routes chat through an enterprise gateway
|
|
1220
|
+
// (docs/environment-variables.md), ahead of the official default. The
|
|
1221
|
+
// Foundry redirect is already handled above.
|
|
1222
|
+
return normalizeAnthropicBaseUrl($env.ANTHROPIC_BASE_URL) ?? configured ?? "https://api.anthropic.com";
|
|
1223
|
+
}
|
|
1224
|
+
return normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function resolveEagerToolInputStreamingSupport(
|
|
1228
|
+
model: Model<"anthropic-messages">,
|
|
1229
|
+
effectiveBaseUrl: string | undefined,
|
|
1230
|
+
): boolean {
|
|
1231
|
+
if (!model.compat.supportsEagerToolInputStreaming) return false;
|
|
1232
|
+
// First-party Anthropic endpoints accept the per-tool flag.
|
|
1233
|
+
if (isOfficialAnthropicApiUrl(effectiveBaseUrl)) return true;
|
|
1234
|
+
// Non-official effective endpoint. `supportsEagerToolInputStreaming` may be
|
|
1235
|
+
// stale-true here because compat is materialized once at build time and is
|
|
1236
|
+
// never rebuilt for a baseUrl-only reroute — either a runtime provider
|
|
1237
|
+
// override (`pi.registerProvider("anthropic", { baseUrl })`) or Foundry
|
|
1238
|
+
// (`CLAUDE_CODE_USE_FOUNDRY`). Both leave the canonical model's resolved
|
|
1239
|
+
// compat in place. `officialEndpoint` records whether compat was built for
|
|
1240
|
+
// the canonical Anthropic URL, so only endpoints whose compat was authored
|
|
1241
|
+
// for a non-official host (an explicit `compat.supportsEagerToolInputStreaming`
|
|
1242
|
+
// opt-in on a custom `baseUrl`) still send the field.
|
|
1243
|
+
return !model.compat.officialEndpoint;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
|
|
1247
|
+
const source = rawHeaders?.trim();
|
|
1248
|
+
if (!source) return undefined;
|
|
1249
|
+
|
|
1250
|
+
const parsed: Record<string, string> = {};
|
|
1251
|
+
for (const token of source.split(/\r?\n|,/)) {
|
|
1252
|
+
const entry = token.trim();
|
|
1253
|
+
if (!entry) continue;
|
|
1254
|
+
const separatorIndex = entry.indexOf(":");
|
|
1255
|
+
if (separatorIndex <= 0) continue;
|
|
1256
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
1257
|
+
const value = entry.slice(separatorIndex + 1).trim();
|
|
1258
|
+
if (!key || !value) continue;
|
|
1259
|
+
parsed[key] = value;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return Object.keys(parsed).length > 0 ? parsed : undefined;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Returns env-supplied custom headers (`ANTHROPIC_CUSTOM_HEADERS`) when they
|
|
1267
|
+
* should be forwarded to the upstream endpoint.
|
|
1268
|
+
*
|
|
1269
|
+
* Foundry mode forwards them unconditionally. Outside Foundry, they're applied
|
|
1270
|
+
* only when the configured base URL is a non-Anthropic host — i.e. an
|
|
1271
|
+
* enterprise/corporate gateway that may require its own proprietary auth
|
|
1272
|
+
* header. Stock `api.anthropic.com` would reject unknown headers, so they're
|
|
1273
|
+
* omitted there.
|
|
1274
|
+
*/
|
|
1275
|
+
export function resolveAnthropicCustomHeadersForBaseUrl(
|
|
1276
|
+
baseUrl: string | undefined,
|
|
1277
|
+
): Record<string, string> | undefined {
|
|
1278
|
+
if (!isFoundryEnabled() && isOfficialAnthropicApiUrl(baseUrl)) return undefined;
|
|
1279
|
+
return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function resolveAnthropicCustomHeaders(
|
|
1283
|
+
model: Model<"anthropic-messages">,
|
|
1284
|
+
baseUrl: string | undefined,
|
|
1285
|
+
): Record<string, string> | undefined {
|
|
1286
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1287
|
+
return resolveAnthropicCustomHeadersForBaseUrl(baseUrl);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function looksLikeFilePath(value: string): boolean {
|
|
1291
|
+
return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function resolvePemValue(value: string | undefined, name: string): string | undefined {
|
|
1295
|
+
const trimmed = value?.trim();
|
|
1296
|
+
if (!trimmed) return undefined;
|
|
1297
|
+
|
|
1298
|
+
const inline = trimmed.replace(/\\n/g, "\n");
|
|
1299
|
+
if (inline.includes("-----BEGIN")) {
|
|
1300
|
+
return inline;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (looksLikeFilePath(trimmed)) {
|
|
1304
|
+
try {
|
|
1305
|
+
return fs.readFileSync(trimmed, "utf8");
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
if (isEnoent(error)) {
|
|
1308
|
+
throw new AIError.ValidationError(`${name} path does not exist: ${trimmed}`);
|
|
1309
|
+
}
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return inline;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
|
|
1318
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1319
|
+
if (!isFoundryEnabled()) return undefined;
|
|
1320
|
+
|
|
1321
|
+
const cacheKey = foundryTlsOptionsCacheKey();
|
|
1322
|
+
if (foundryTlsOptionsCache.has(cacheKey)) return foundryTlsOptionsCache.get(cacheKey);
|
|
1323
|
+
|
|
1324
|
+
const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
|
|
1325
|
+
const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
|
|
1326
|
+
const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
|
|
1327
|
+
|
|
1328
|
+
if ((cert && !key) || (!cert && key)) {
|
|
1329
|
+
throw new AIError.ConfigurationError(
|
|
1330
|
+
"Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.",
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const options: FoundryTlsOptions = {};
|
|
1335
|
+
if (ca) options.ca = [...tls.rootCertificates, ca];
|
|
1336
|
+
if (cert) options.cert = cert;
|
|
1337
|
+
if (key) options.key = key;
|
|
1338
|
+
const resolved = Object.keys(options).length > 0 ? options : undefined;
|
|
1339
|
+
foundryTlsOptionsCache.set(cacheKey, resolved);
|
|
1340
|
+
return resolved;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function buildCoworkTlsFetchOptions(
|
|
1344
|
+
model: Model<"anthropic-messages">,
|
|
1345
|
+
baseUrl: string | undefined,
|
|
1346
|
+
): AnthropicFetchOptions | undefined {
|
|
1347
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1348
|
+
if (!baseUrl) return undefined;
|
|
1349
|
+
|
|
1350
|
+
let serverName: string;
|
|
1351
|
+
try {
|
|
1352
|
+
serverName = new URL(baseUrl).hostname;
|
|
1353
|
+
} catch {
|
|
1354
|
+
return undefined;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (!serverName) return undefined;
|
|
1358
|
+
|
|
1359
|
+
const foundryTlsOptions = resolveFoundryTlsOptions(model);
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1362
|
+
tls: {
|
|
1363
|
+
rejectUnauthorized: true,
|
|
1364
|
+
serverName,
|
|
1365
|
+
...(COWORK_TLS_CIPHERS ? { ciphers: COWORK_TLS_CIPHERS } : {}),
|
|
1366
|
+
...(foundryTlsOptions ?? {}),
|
|
1367
|
+
},
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
|
|
1371
|
+
// Case-insensitive merge: later sources win and keep their casing. A plain
|
|
1372
|
+
// Object.assign would let `authorization` and `Authorization` coexist, and
|
|
1373
|
+
// the Headers constructor then joins both values comma-separated on the wire.
|
|
1374
|
+
const merged: Record<string, string> = {};
|
|
1375
|
+
const keyByLower = new Map<string, string>();
|
|
1376
|
+
for (const headers of headerSources) {
|
|
1377
|
+
if (!headers) continue;
|
|
1378
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1379
|
+
const lower = key.toLowerCase();
|
|
1380
|
+
const existing = keyByLower.get(lower);
|
|
1381
|
+
if (existing !== undefined && existing !== key) delete merged[existing];
|
|
1382
|
+
keyByLower.set(lower, key);
|
|
1383
|
+
merged[key] = value;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return merged;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
|
|
1390
|
+
"message_start",
|
|
1391
|
+
"message_delta",
|
|
1392
|
+
"message_stop",
|
|
1393
|
+
"content_block_start",
|
|
1394
|
+
"content_block_delta",
|
|
1395
|
+
"content_block_stop",
|
|
1396
|
+
]);
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Iterate over Anthropic SSE events from a raw Response, preserving ping events
|
|
1400
|
+
* for liveness. Malformed event envelopes are logged and skipped (non-fatal)
|
|
1401
|
+
* rather than aborting the stream.
|
|
1402
|
+
*/
|
|
1403
|
+
type RawMessagePingEvent = { type: "ping" };
|
|
1404
|
+
type AnthropicStreamEvent = RawMessageStreamEvent | RawMessagePingEvent;
|
|
1405
|
+
const ANTHROPIC_PING_EVENT: RawMessagePingEvent = { type: "ping" };
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* In-stream `error` SSE frames carry an Anthropic error envelope:
|
|
1409
|
+
* `{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}`.
|
|
1410
|
+
* Surface the structured type + message instead of the raw JSON blob; the
|
|
1411
|
+
* error type token (e.g. `overloaded_error`, `rate_limit_error`) is kept in
|
|
1412
|
+
* the message so `isProviderRetryableError`'s classification keys off the
|
|
1413
|
+
* structured type rather than incidental JSON substrings.
|
|
1414
|
+
*/
|
|
1415
|
+
function createAnthropicSseStreamError(data: string): Error {
|
|
1416
|
+
try {
|
|
1417
|
+
const parsed = JSON.parse(data) as { error?: { type?: unknown; message?: unknown } };
|
|
1418
|
+
const errorType = typeof parsed?.error?.type === "string" ? parsed.error.type : undefined;
|
|
1419
|
+
const message = typeof parsed?.error?.message === "string" ? parsed.error.message : undefined;
|
|
1420
|
+
if (message) {
|
|
1421
|
+
return new AIError.ProviderResponseError(
|
|
1422
|
+
errorType ? `Anthropic stream error (${errorType}): ${message}` : `Anthropic stream error: ${message}`,
|
|
1423
|
+
{ provider: "anthropic", kind: "output" },
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
} catch {
|
|
1427
|
+
// Not a JSON envelope; fall through to the raw payload.
|
|
1428
|
+
}
|
|
1429
|
+
return new AIError.ProviderResponseError(data, { provider: "anthropic", kind: "output" });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async function* iterateAnthropicEvents(
|
|
1433
|
+
response: Response,
|
|
1434
|
+
signal?: AbortSignal,
|
|
1435
|
+
onSseEvent?: AnthropicOptions["onSseEvent"],
|
|
1436
|
+
): AsyncGenerator<AnthropicStreamEvent> {
|
|
1437
|
+
if (!response.body) {
|
|
1438
|
+
throw new AIError.AnthropicStreamEnvelopeError("Attempted to iterate over an Anthropic response with no body");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
let sawMessageStart = false;
|
|
1442
|
+
let sawMessageEnd = false;
|
|
1443
|
+
|
|
1444
|
+
for await (const sse of readSseEvents(response.body, signal)) {
|
|
1445
|
+
notifyRawSseEvent(onSseEvent, sse);
|
|
1446
|
+
if (sse.event === "error") {
|
|
1447
|
+
throw createAnthropicSseStreamError(sse.data);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (sse.event === "ping") {
|
|
1451
|
+
// Surface keepalives so the idle watchdog treats them as liveness.
|
|
1452
|
+
yield ANTHROPIC_PING_EVENT;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
try {
|
|
1461
|
+
const event = JSON.parse(sse.data) as RawMessageStreamEvent;
|
|
1462
|
+
if (event.type !== sse.event) {
|
|
1463
|
+
reportAnthropicEnvelopeAnomaly(`event type ${event.type} does not match SSE event ${sse.event}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (event.type === "message_start") {
|
|
1466
|
+
sawMessageStart = true;
|
|
1467
|
+
} else if (event.type === "message_stop") {
|
|
1468
|
+
sawMessageEnd = true;
|
|
1469
|
+
}
|
|
1470
|
+
yield event;
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1473
|
+
reportAnthropicEnvelopeAnomaly(
|
|
1474
|
+
`could not parse SSE event ${sse.event}: ${message}; skipping frame; data=${sse.data}`,
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (sawMessageStart && !sawMessageEnd && !signal?.aborted) {
|
|
1480
|
+
reportAnthropicEnvelopeAnomaly("stream ended before message_stop");
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
type AnthropicRawResponseRequest = {
|
|
1485
|
+
asResponse(): Promise<Response>;
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
function hasAnthropicRawResponseRequest(request: unknown): request is AnthropicRawResponseRequest {
|
|
1489
|
+
return isRecord(request) && typeof request.asResponse === "function";
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
type AnthropicStreamWithResponseRequest = {
|
|
1493
|
+
withResponse(): Promise<{
|
|
1494
|
+
data: AsyncIterable<RawMessageStreamEvent>;
|
|
1495
|
+
response: Response;
|
|
1496
|
+
request_id: string | null;
|
|
1497
|
+
}>;
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
function hasAnthropicStreamWithResponseRequest(request: unknown): request is AnthropicStreamWithResponseRequest {
|
|
1501
|
+
return isRecord(request) && typeof request.withResponse === "function";
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function getAnthropicStreamResponse(
|
|
1505
|
+
request: unknown,
|
|
1506
|
+
signal?: AbortSignal,
|
|
1507
|
+
onSseEvent?: AnthropicOptions["onSseEvent"],
|
|
1508
|
+
): Promise<{
|
|
1509
|
+
events: AsyncIterable<AnthropicStreamEvent>;
|
|
1510
|
+
response: Response;
|
|
1511
|
+
requestId: string | null;
|
|
1512
|
+
recordsRawSseEvents: boolean;
|
|
1513
|
+
}> {
|
|
1514
|
+
if (hasAnthropicRawResponseRequest(request)) {
|
|
1515
|
+
const response = await request.asResponse();
|
|
1516
|
+
return {
|
|
1517
|
+
events: iterateAnthropicEvents(response, signal, onSseEvent),
|
|
1518
|
+
response,
|
|
1519
|
+
requestId: response.headers.get("request-id"),
|
|
1520
|
+
recordsRawSseEvents: true,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
if (hasAnthropicStreamWithResponseRequest(request)) {
|
|
1524
|
+
const { data, response, request_id } = await request.withResponse();
|
|
1525
|
+
return { events: data, response, requestId: request_id, recordsRawSseEvents: false };
|
|
1526
|
+
}
|
|
1527
|
+
throw new AIError.AnthropicStreamEnvelopeError("Anthropic SDK request did not expose a stream response");
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
async function* observeDecodedAnthropicSdkEvents(
|
|
1531
|
+
events: AsyncIterable<AnthropicStreamEvent>,
|
|
1532
|
+
observer: (event: RawSseEvent) => void,
|
|
1533
|
+
): AsyncGenerator<AnthropicStreamEvent> {
|
|
1534
|
+
for await (const event of events) {
|
|
1535
|
+
const data = JSON.stringify(event);
|
|
1536
|
+
// Reconstructed from decoded SDK event; not literal wire bytes.
|
|
1537
|
+
notifyRawSseEvent(observer, { event: event.type, data, raw: [`event: ${event.type}`, `data: ${data}`] });
|
|
1538
|
+
yield event;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const PROVIDER_MAX_RETRIES = 10;
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Flat delay between attempts when Copilot 400s a model its own `/models`
|
|
1546
|
+
* catalog advertises. Part of the fleet carries the model and part doesn't, so
|
|
1547
|
+
* the retry is a reroll rather than a wait for capacity to free up.
|
|
1548
|
+
*/
|
|
1549
|
+
const COPILOT_MODEL_FLAP_RETRY_DELAY_MS = 400;
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* How long `ping` keepalives may keep extending the idle deadline without any
|
|
1553
|
+
* semantic stream progress, as a multiple of the idle timeout. Anthropic pings
|
|
1554
|
+
* across legitimate generation gaps, so pings count as liveness — but a wedged
|
|
1555
|
+
* upstream that pings forever while producing no events must eventually trip
|
|
1556
|
+
* the idle watchdog instead of hanging an active tool-call stream without a
|
|
1557
|
+
* recovery path (#4900).
|
|
1558
|
+
*/
|
|
1559
|
+
const PING_PROGRESS_MAX_IDLE_MULTIPLIER = 3;
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Log a malformed-stream-envelope anomaly without aborting the turn. The strict
|
|
1563
|
+
* parser would `throw new AnthropicStreamEnvelopeError(...)` here; we instead
|
|
1564
|
+
* surface a warning and let the caller skip the offending event (or finalize what
|
|
1565
|
+
* already streamed) so a non-conforming endpoint degrades to best-effort content
|
|
1566
|
+
* rather than failing the request.
|
|
1567
|
+
*/
|
|
1568
|
+
function reportAnthropicEnvelopeAnomaly(detail: string): void {
|
|
1569
|
+
logger.warn(`anthropic: ignoring malformed stream envelope: ${detail}`);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
|
|
1573
|
+
if (typeof eventType !== "string") return false;
|
|
1574
|
+
if (eventType === "ping") return true;
|
|
1575
|
+
return !ANTHROPIC_MESSAGE_EVENTS.has(eventType);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Whether an Anthropic (or Copilot-over-Anthropic) stream error should be
|
|
1580
|
+
* retried. The classification lives in {@link AIError.isProviderRetryableError};
|
|
1581
|
+
* this wrapper injects the Copilot-specific model-availability transient check,
|
|
1582
|
+
* which the error module must not import directly.
|
|
1583
|
+
*/
|
|
1584
|
+
export function isProviderRetryableError(error: unknown, provider?: string): boolean {
|
|
1585
|
+
return AIError.isProviderRetryableError(error, {
|
|
1586
|
+
provider,
|
|
1587
|
+
isProviderTransient:
|
|
1588
|
+
provider === "github-copilot" ? (err): boolean => AIError.isCopilotTransientModelError(err) : undefined,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const THINKING_ENVELOPE_OPEN = "<thinking>";
|
|
1593
|
+
const THINKING_ENVELOPE_CLOSE = "</thinking>";
|
|
1594
|
+
|
|
1595
|
+
function unwrapAnthropicThinkingEnvelope(text: string): string | undefined {
|
|
1596
|
+
let current = text.trim();
|
|
1597
|
+
let stripped = false;
|
|
1598
|
+
while (current.startsWith(THINKING_ENVELOPE_OPEN) && current.endsWith(THINKING_ENVELOPE_CLOSE)) {
|
|
1599
|
+
current = current.slice(THINKING_ENVELOPE_OPEN.length, current.length - THINKING_ENVELOPE_CLOSE.length).trim();
|
|
1600
|
+
stripped = true;
|
|
1601
|
+
}
|
|
1602
|
+
return stripped ? current : undefined;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function createEmptyUsage(premiumRequests?: number): Usage {
|
|
1606
|
+
return {
|
|
1607
|
+
input: 0,
|
|
1608
|
+
output: 0,
|
|
1609
|
+
cacheRead: 0,
|
|
1610
|
+
cacheWrite: 0,
|
|
1611
|
+
totalTokens: 0,
|
|
1612
|
+
...(premiumRequests === undefined ? {} : { premiumRequests }),
|
|
1613
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
export type AnthropicUsageLike = {
|
|
1618
|
+
cache_creation?: { ephemeral_5m_input_tokens?: number | null; ephemeral_1h_input_tokens?: number | null } | null;
|
|
1619
|
+
server_tool_use?: { web_search_requests?: number | null; web_fetch_requests?: number | null } | null;
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Capture Anthropic's optional cache-creation TTL breakdown and server-tool-use
|
|
1624
|
+
* counters into the harness Usage shape. Omitted/null fields are no-ops; explicit
|
|
1625
|
+
* zero-valued objects clear prior extras from earlier stream usage snapshots.
|
|
1626
|
+
*/
|
|
1627
|
+
export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLike): void {
|
|
1628
|
+
const cacheCreation = source.cache_creation;
|
|
1629
|
+
if (cacheCreation != null) {
|
|
1630
|
+
const fiveMinute = cacheCreation.ephemeral_5m_input_tokens ?? 0;
|
|
1631
|
+
const oneHour = cacheCreation.ephemeral_1h_input_tokens ?? 0;
|
|
1632
|
+
if (fiveMinute > 0 || oneHour > 0) {
|
|
1633
|
+
usage.cttl = {
|
|
1634
|
+
...(fiveMinute > 0 ? { ephemeral5m: fiveMinute } : {}),
|
|
1635
|
+
...(oneHour > 0 ? { ephemeral1h: oneHour } : {}),
|
|
1636
|
+
};
|
|
1637
|
+
} else {
|
|
1638
|
+
delete usage.cttl;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const serverToolUse = source.server_tool_use;
|
|
1642
|
+
if (serverToolUse != null) {
|
|
1643
|
+
const webSearch = serverToolUse.web_search_requests ?? 0;
|
|
1644
|
+
const webFetch = serverToolUse.web_fetch_requests ?? 0;
|
|
1645
|
+
if (webSearch > 0 || webFetch > 0) {
|
|
1646
|
+
usage.server = {
|
|
1647
|
+
...(webSearch > 0 ? { webSearch } : {}),
|
|
1648
|
+
...(webFetch > 0 ? { webFetch } : {}),
|
|
1649
|
+
};
|
|
1650
|
+
} else {
|
|
1651
|
+
delete usage.server;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function parseAnthropicFallbackWireBlock(value: unknown): AnthropicFallbackContent | undefined {
|
|
1657
|
+
if (!isRecord(value) || value.type !== "fallback") return undefined;
|
|
1658
|
+
const from = isRecord(value.from) && typeof value.from.model === "string" ? value.from.model : undefined;
|
|
1659
|
+
const to = isRecord(value.to) && typeof value.to.model === "string" ? value.to.model : undefined;
|
|
1660
|
+
if (!from?.trim() || !to?.trim()) return undefined;
|
|
1661
|
+
return { type: "fallback", from: { model: from }, to: { model: to } };
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* The definitive "served by fallback" signal per Anthropic's fallback
|
|
1666
|
+
* billing cookbook (§4): a `fallback_message` iteration in `usage.iterations`.
|
|
1667
|
+
* Any other iteration type is per-attempt bookkeeping for the requested model
|
|
1668
|
+
* (including its dated snapshot alias) and MUST NOT retag the assistant turn.
|
|
1669
|
+
*/
|
|
1670
|
+
function fallbackServedModelFromUsage(source: AnthropicWireUsage): string | undefined {
|
|
1671
|
+
const iterations = source.iterations ?? [];
|
|
1672
|
+
for (let index = iterations.length - 1; index >= 0; index -= 1) {
|
|
1673
|
+
const iteration = iterations[index];
|
|
1674
|
+
if (iteration?.type === "fallback_message" && iteration.model?.trim()) return iteration.model;
|
|
1675
|
+
}
|
|
1676
|
+
return undefined;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Price a fallback turn per the fallback billing cookbook §4:
|
|
1681
|
+
* • A pre-served attempt with zero output/cache-creation is not billed
|
|
1682
|
+
* (waived classifier block); its iteration is skipped.
|
|
1683
|
+
* • Mid-stream refusals bill their attempting model's input+output at
|
|
1684
|
+
* that model's normal rates.
|
|
1685
|
+
* • The `fallback_message` attempt's input tokens are rebilled at the
|
|
1686
|
+
* served model's cache-read rate (fallback credit — 10% of base input).
|
|
1687
|
+
*
|
|
1688
|
+
* Top-level `usage.input/output/cacheRead/cacheWrite` stay Anthropic's raw
|
|
1689
|
+
* served-attempt counts; `usage.cost` reflects the per-iteration attributed
|
|
1690
|
+
* total. Non-fallback turns skip this path entirely and use the requested
|
|
1691
|
+
* model at the normal `calculateCost` call.
|
|
1692
|
+
*/
|
|
1693
|
+
/**
|
|
1694
|
+
* Resolve a served/iteration model id to its bundled catalog entry when
|
|
1695
|
+
* possible so the per-iteration cost uses the served model's pricing
|
|
1696
|
+
* (e.g. Opus 4.8 rates for a Fable→Opus fallback). Falls back to
|
|
1697
|
+
* `requestModel` when the id is empty, matches the request, or the
|
|
1698
|
+
* catalog has no entry under it — the caller keeps the requested-model
|
|
1699
|
+
* pricing as the safe default and logs at the source.
|
|
1700
|
+
*/
|
|
1701
|
+
function resolveIterationModel(
|
|
1702
|
+
requestModel: Model<"anthropic-messages">,
|
|
1703
|
+
iterationModelId: string | null | undefined,
|
|
1704
|
+
): Model<Api> {
|
|
1705
|
+
const id = iterationModelId?.trim();
|
|
1706
|
+
if (!id || id === requestModel.id) return requestModel;
|
|
1707
|
+
// Bundled catalog lookup: only Anthropic provider entries are safe to
|
|
1708
|
+
// reference (dated snapshots resolve to their alias entry when present).
|
|
1709
|
+
if (requestModel.provider === "anthropic") {
|
|
1710
|
+
const bundled = getBundledModel("anthropic", id);
|
|
1711
|
+
if (bundled?.api === "anthropic-messages") return bundled;
|
|
1712
|
+
}
|
|
1713
|
+
return requestModel;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function calculateFallbackTurnCost(
|
|
1717
|
+
requestModel: Model<"anthropic-messages">,
|
|
1718
|
+
usage: Usage,
|
|
1719
|
+
source: AnthropicWireUsage,
|
|
1720
|
+
): boolean {
|
|
1721
|
+
const iterations = source.iterations ?? [];
|
|
1722
|
+
if (iterations.length === 0) return false;
|
|
1723
|
+
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
1724
|
+
const hasFallbackMessage = iterations.some(iter => iter.type === "fallback_message");
|
|
1725
|
+
let applied = false;
|
|
1726
|
+
for (const iteration of iterations) {
|
|
1727
|
+
const inputTokens = iteration.input_tokens ?? 0;
|
|
1728
|
+
const outputTokens = iteration.output_tokens ?? 0;
|
|
1729
|
+
const cacheReadTokens = iteration.cache_read_input_tokens ?? 0;
|
|
1730
|
+
const cacheWriteTokens = iteration.cache_creation_input_tokens ?? 0;
|
|
1731
|
+
const isFallback = iteration.type === "fallback_message";
|
|
1732
|
+
if (hasFallbackMessage && !isFallback && outputTokens === 0 && cacheWriteTokens === 0) continue;
|
|
1733
|
+
const iterationUsage = createEmptyUsage();
|
|
1734
|
+
if (isFallback) {
|
|
1735
|
+
iterationUsage.input = 0;
|
|
1736
|
+
iterationUsage.cacheRead = cacheReadTokens + inputTokens;
|
|
1737
|
+
} else {
|
|
1738
|
+
iterationUsage.input = inputTokens;
|
|
1739
|
+
iterationUsage.cacheRead = cacheReadTokens;
|
|
1740
|
+
}
|
|
1741
|
+
iterationUsage.output = outputTokens;
|
|
1742
|
+
iterationUsage.cacheWrite = cacheWriteTokens;
|
|
1743
|
+
iterationUsage.totalTokens =
|
|
1744
|
+
iterationUsage.input + iterationUsage.output + iterationUsage.cacheRead + iterationUsage.cacheWrite;
|
|
1745
|
+
calculateCost(resolveIterationModel(requestModel, iteration.model), iterationUsage);
|
|
1746
|
+
cost.input += iterationUsage.cost.input;
|
|
1747
|
+
cost.output += iterationUsage.cost.output;
|
|
1748
|
+
cost.cacheRead += iterationUsage.cost.cacheRead;
|
|
1749
|
+
cost.cacheWrite += iterationUsage.cost.cacheWrite;
|
|
1750
|
+
cost.total += iterationUsage.cost.total;
|
|
1751
|
+
applied = true;
|
|
1752
|
+
}
|
|
1753
|
+
if (!applied) return false;
|
|
1754
|
+
usage.cost = cost;
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Detects the Anthropic `400 Invalid `signature` in `thinking` block` failure
|
|
1760
|
+
* a signing proxy returns when a stripped/unsigned prior thinking block is
|
|
1761
|
+
* replayed as `signature: ""`. Exported for the compat tests.
|
|
1762
|
+
*/
|
|
1763
|
+
const INVALID_THINKING_SIGNATURE_PATTERN = /invalid\s+`?signature`?\s+in\s+`?thinking`?(?:\s+block)?/i;
|
|
1764
|
+
export function isInvalidThinkingSignatureError(message: string): boolean {
|
|
1765
|
+
return INVALID_THINKING_SIGNATURE_PATTERN.test(message);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Prepend a pointed remediation to Anthropic's `Invalid signature in thinking
|
|
1770
|
+
* block` 400 when the model looks like an unmarked custom signing proxy
|
|
1771
|
+
* (opaque baseUrl, `spec.reasoning: true`, no explicit
|
|
1772
|
+
* `compat.replayUnsignedThinking` override). The default is native replay for
|
|
1773
|
+
* the 3p reasoning majority (#2005); this hint turns the misconfigured-proxy
|
|
1774
|
+
* case into a one-line fix instead of a silent retry loop (#4297).
|
|
1775
|
+
*/
|
|
1776
|
+
export function maybeAddReplayUnsignedThinkingHint(model: Model<"anthropic-messages">, message: string): string {
|
|
1777
|
+
if (!isInvalidThinkingSignatureError(message)) return message;
|
|
1778
|
+
if (model.compat.officialEndpoint) return message;
|
|
1779
|
+
if (model.compatConfig?.replayUnsignedThinking !== undefined) return message;
|
|
1780
|
+
const hint = `Provider "${model.provider}" looks like an Anthropic-compatible signing proxy: it rejected a replayed unsigned thinking block. Set \`compat.replayUnsignedThinking: false\` under \`providers.${model.provider}\` in your models.yml and retry. See https://github.com/can1357/oh-my-pi/issues/4297.`;
|
|
1781
|
+
return `${hint}\n\n${message}`;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const streamAnthropicOnce = (
|
|
1785
|
+
model: Model<"anthropic-messages">,
|
|
1786
|
+
context: Context,
|
|
1787
|
+
options?: AnthropicOptions,
|
|
1788
|
+
): AssistantMessageEventStream => {
|
|
1789
|
+
const stream = new AssistantMessageEventStream();
|
|
1790
|
+
|
|
1791
|
+
(async () => {
|
|
1792
|
+
const startTime = performance.now();
|
|
1793
|
+
let firstTokenTime: number | undefined;
|
|
1794
|
+
|
|
1795
|
+
const output: AssistantMessage = {
|
|
1796
|
+
role: "assistant",
|
|
1797
|
+
content: [],
|
|
1798
|
+
api: model.api as Api,
|
|
1799
|
+
provider: model.provider,
|
|
1800
|
+
model: model.id,
|
|
1801
|
+
usage: createEmptyUsage(),
|
|
1802
|
+
stopReason: "stop",
|
|
1803
|
+
timestamp: Date.now(),
|
|
1804
|
+
};
|
|
1805
|
+
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
1806
|
+
let activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
1807
|
+
|
|
1808
|
+
const onSseEvent = options?.onSseEvent;
|
|
1809
|
+
const rawSseObserver = onSseEvent ? (event: RawSseEvent) => onSseEvent(event, model) : undefined;
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
// Built inside the try so a copilot credential/header failure surfaces as
|
|
1813
|
+
// an error event instead of an unhandled rejection that leaves the stream
|
|
1814
|
+
// (and any consumer awaiting `result()`) hanging forever.
|
|
1815
|
+
const copilotDynamicHeaders =
|
|
1816
|
+
model.provider === "github-copilot"
|
|
1817
|
+
? buildCopilotDynamicHeaders({
|
|
1818
|
+
messages: context.messages,
|
|
1819
|
+
hasImages: hasCopilotVisionInput(context.messages),
|
|
1820
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
1821
|
+
headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
|
|
1822
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1823
|
+
})
|
|
1824
|
+
: undefined;
|
|
1825
|
+
if (copilotDynamicHeaders?.premiumRequests !== undefined) {
|
|
1826
|
+
output.usage.premiumRequests = copilotDynamicHeaders.premiumRequests;
|
|
1827
|
+
}
|
|
1828
|
+
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
|
1829
|
+
const baseUrl = resolveAnthropicBaseUrl(model, apiKey) ?? "https://api.anthropic.com";
|
|
1830
|
+
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
|
1831
|
+
const providerSessionState = getAnthropicProviderSessionState(
|
|
1832
|
+
options?.providerSessionState,
|
|
1833
|
+
baseUrl,
|
|
1834
|
+
model.id,
|
|
1835
|
+
);
|
|
1836
|
+
let disableStrictTools =
|
|
1837
|
+
(providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
|
|
1838
|
+
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
1839
|
+
let forceDemoteUnsignedThinking = providerSessionState?.replayUnsignedThinkingDisabled ?? false;
|
|
1840
|
+
const mergedCallerHeaders = mergeHeaders(model.headers, options?.headers);
|
|
1841
|
+
const umansGatewayWebSearchHeader = getUmansWebSearchHeader(model, mergedCallerHeaders);
|
|
1842
|
+
// Keep fallback payloads aligned with the top-level Vertex effort gate:
|
|
1843
|
+
// no nested effort field means the fallback scan cannot re-add its beta.
|
|
1844
|
+
let fallbacks = options?.fallbacks;
|
|
1845
|
+
if (
|
|
1846
|
+
model.provider === "google-vertex" &&
|
|
1847
|
+
fallbacks?.some(entry => entry.output_config?.effort !== undefined)
|
|
1848
|
+
) {
|
|
1849
|
+
fallbacks = fallbacks.map(entry => {
|
|
1850
|
+
const outputConfig = entry.output_config;
|
|
1851
|
+
if (outputConfig?.effort === undefined) return entry;
|
|
1852
|
+
return {
|
|
1853
|
+
...entry,
|
|
1854
|
+
output_config:
|
|
1855
|
+
outputConfig.task_budget === undefined ? undefined : { task_budget: outputConfig.task_budget },
|
|
1856
|
+
};
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
let client: AnthropicMessagesClientLike;
|
|
1861
|
+
let isOAuthToken: boolean;
|
|
1862
|
+
|
|
1863
|
+
if (options?.client) {
|
|
1864
|
+
client = options.client;
|
|
1865
|
+
isOAuthToken = false;
|
|
1866
|
+
} else {
|
|
1867
|
+
const extraBetas = normalizeExtraBetas(options?.betas);
|
|
1868
|
+
const wantsAnthropicPriority = model.provider === "anthropic" && options?.serviceTier === "priority";
|
|
1869
|
+
// Skip the fast-mode beta when this session already learned the
|
|
1870
|
+
// endpoint+model rejects fast mode; `speed` is dropped from the params
|
|
1871
|
+
// too (dropFastMode), so the request stays a faithful non-fast request.
|
|
1872
|
+
if (wantsAnthropicPriority && !dropFastMode && !extraBetas.includes(fastModeBeta)) {
|
|
1873
|
+
extraBetas.push(fastModeBeta);
|
|
1874
|
+
}
|
|
1875
|
+
if (options?.taskBudget && !extraBetas.includes(taskBudgetBeta)) {
|
|
1876
|
+
extraBetas.push(taskBudgetBeta);
|
|
1877
|
+
}
|
|
1878
|
+
// `output_config.effort` ships on thinking-on requests, explicit
|
|
1879
|
+
// thinking-off adaptive pins, and forced-tool adaptive pins. The beta
|
|
1880
|
+
// must accompany the field even when direct streamAnthropic callers omit
|
|
1881
|
+
// thinkingEnabled (#6589). MiniMax uses `thinking.type:"adaptive"` itself
|
|
1882
|
+
// as the control surface, so the sentinel "adaptive" value intentionally
|
|
1883
|
+
// sends no output_config. Skip Vertex rawPredict: that adapter needs betas
|
|
1884
|
+
// in the body (`anthropic_beta`), not as an `anthropic-beta` HTTP header,
|
|
1885
|
+
// so the effort field is dropped from the body there too (see buildParams)
|
|
1886
|
+
// and advertising the beta would only earn a 400 (#5614).
|
|
1887
|
+
const sendsAdaptiveEffortPin =
|
|
1888
|
+
isAdaptiveOnlyThinking(model) &&
|
|
1889
|
+
(options?.thinkingEnabled === false ||
|
|
1890
|
+
(model.compat.supportsForcedToolChoice && isForcedToolChoice(options?.toolChoice)));
|
|
1891
|
+
if (
|
|
1892
|
+
model.reasoning &&
|
|
1893
|
+
model.provider !== "google-vertex" &&
|
|
1894
|
+
((options?.thinkingEnabled && options.effort !== "adaptive") || sendsAdaptiveEffortPin) &&
|
|
1895
|
+
!extraBetas.includes(effortBeta)
|
|
1896
|
+
) {
|
|
1897
|
+
extraBetas.push(effortBeta);
|
|
1898
|
+
}
|
|
1899
|
+
if (model.compat.supportsMidConversationSystem && !extraBetas.includes(midConversationSystemBeta)) {
|
|
1900
|
+
// convertAnthropicMessages may upgrade developer turns to the
|
|
1901
|
+
// mid-conversation `system` role on these models; API-key requests
|
|
1902
|
+
// need the beta alongside the role (OAuth agent requests already
|
|
1903
|
+
// carry it in the Claude Code list).
|
|
1904
|
+
extraBetas.push(midConversationSystemBeta);
|
|
1905
|
+
}
|
|
1906
|
+
// `context_management.clear_thinking_20251015` requires this beta. OAuth
|
|
1907
|
+
// requests carry it in `claudeCodeAgentBetaDefaults`; API-key requests
|
|
1908
|
+
// need it added explicitly so the field is honored instead of rejected
|
|
1909
|
+
// (#3288). Skip transports where this package cannot deliver or the
|
|
1910
|
+
// provider cannot accept the beta: Copilot strips Anthropic betas;
|
|
1911
|
+
// Vertex rawPredict needs betas in the body (`anthropic_beta`), not as
|
|
1912
|
+
// an `anthropic-beta` HTTP header; and OpenCode Zen rejects the related
|
|
1913
|
+
// `context_management` field (#6510).
|
|
1914
|
+
if (
|
|
1915
|
+
model.reasoning &&
|
|
1916
|
+
options?.thinkingEnabled &&
|
|
1917
|
+
model.provider !== "github-copilot" &&
|
|
1918
|
+
model.provider !== "google-vertex" &&
|
|
1919
|
+
model.provider !== "opencode-zen" &&
|
|
1920
|
+
!extraBetas.includes(contextManagementBeta)
|
|
1921
|
+
) {
|
|
1922
|
+
extraBetas.push(contextManagementBeta);
|
|
1923
|
+
}
|
|
1924
|
+
// `ttl: "1h"` requires the extended-cache-ttl beta on API-key
|
|
1925
|
+
// requests. OAuth requests never add it here: agent requests
|
|
1926
|
+
// already carry it in the Claude Code beta list, and utility
|
|
1927
|
+
// requests must not deviate from CC's header fingerprint.
|
|
1928
|
+
if (
|
|
1929
|
+
!(options?.isOAuth ?? isAnthropicOAuthToken(apiKey)) &&
|
|
1930
|
+
getCacheControl(model, options?.cacheRetention, false).cacheControl?.ttl === "1h" &&
|
|
1931
|
+
!extraBetas.includes(extendedCacheTtlBeta)
|
|
1932
|
+
) {
|
|
1933
|
+
extraBetas.push(extendedCacheTtlBeta);
|
|
1934
|
+
}
|
|
1935
|
+
// Server-side fallback beta chain: opt-in via `options.fallbacks`.
|
|
1936
|
+
// Nested overrides (`speed`, `output_config.effort`,
|
|
1937
|
+
// `output_config.task_budget`) reuse the same top-level betas
|
|
1938
|
+
// Anthropic requires for the primary request, so scan the chain
|
|
1939
|
+
// and add every companion beta the fallback entries touch.
|
|
1940
|
+
if (fallbacks?.length) {
|
|
1941
|
+
if (!extraBetas.includes(serverSideFallbackBeta)) {
|
|
1942
|
+
extraBetas.push(serverSideFallbackBeta);
|
|
1943
|
+
}
|
|
1944
|
+
for (const entry of fallbacks) {
|
|
1945
|
+
if (entry.speed === "fast" && !extraBetas.includes(fastModeBeta)) {
|
|
1946
|
+
extraBetas.push(fastModeBeta);
|
|
1947
|
+
}
|
|
1948
|
+
if (entry.output_config?.effort && !extraBetas.includes(effortBeta)) {
|
|
1949
|
+
extraBetas.push(effortBeta);
|
|
1950
|
+
}
|
|
1951
|
+
if (entry.output_config?.task_budget && !extraBetas.includes(taskBudgetBeta)) {
|
|
1952
|
+
extraBetas.push(taskBudgetBeta);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const created = createClient(model, {
|
|
1958
|
+
model,
|
|
1959
|
+
apiKey,
|
|
1960
|
+
extraBetas,
|
|
1961
|
+
stream: true,
|
|
1962
|
+
interleavedThinking: options?.interleavedThinking ?? true,
|
|
1963
|
+
headers: options?.headers,
|
|
1964
|
+
dynamicHeaders: copilotDynamicHeaders?.headers,
|
|
1965
|
+
isOAuth: options?.isOAuth,
|
|
1966
|
+
hasTools: !!context.tools?.length,
|
|
1967
|
+
thinkingEnabled: options?.thinkingEnabled,
|
|
1968
|
+
thinkingDisplay: options?.thinkingDisplay,
|
|
1969
|
+
fetch: options?.fetch,
|
|
1970
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1971
|
+
claudeCodeSessionId: options?.sessionId ?? extractClaudeMetadataSessionId(options?.metadata?.user_id),
|
|
1972
|
+
disableStrictTools,
|
|
1973
|
+
});
|
|
1974
|
+
client = created.client;
|
|
1975
|
+
isOAuthToken = created.isOAuthToken;
|
|
1976
|
+
}
|
|
1977
|
+
const preparedContext = await prepareAnthropicManyImageContext(context, model.input.includes("image"));
|
|
1978
|
+
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
1979
|
+
let nextParams = buildParams(model, preparedContext, isOAuthToken, options, {
|
|
1980
|
+
disableStrictTools,
|
|
1981
|
+
useUmansGatewayWebSearch: umansGatewayWebSearchHeader !== undefined,
|
|
1982
|
+
forceDemoteUnsignedThinking,
|
|
1983
|
+
supportsEagerToolInputStreaming,
|
|
1984
|
+
fallbacks,
|
|
1985
|
+
});
|
|
1986
|
+
if (disableStrictTools) {
|
|
1987
|
+
dropAnthropicStrictTools(nextParams);
|
|
1988
|
+
}
|
|
1989
|
+
if (dropFastMode) {
|
|
1990
|
+
dropAnthropicFastMode(nextParams);
|
|
1991
|
+
}
|
|
1992
|
+
const replacementPayload = await options?.onPayload?.(nextParams, model);
|
|
1993
|
+
if (replacementPayload !== undefined) {
|
|
1994
|
+
nextParams = replacementPayload as typeof nextParams;
|
|
1995
|
+
}
|
|
1996
|
+
nextParams = toWellFormedDeep(nextParams) as typeof nextParams;
|
|
1997
|
+
rawRequestDump = {
|
|
1998
|
+
provider: model.provider,
|
|
1999
|
+
api: output.api,
|
|
2000
|
+
model: model.id,
|
|
2001
|
+
method: "POST",
|
|
2002
|
+
url: `${baseUrl}/v1/messages${isOAuthToken ? "?beta=true" : ""}`,
|
|
2003
|
+
body: nextParams,
|
|
2004
|
+
};
|
|
2005
|
+
return nextParams;
|
|
2006
|
+
};
|
|
2007
|
+
let params = await prepareParams();
|
|
2008
|
+
|
|
2009
|
+
// Opt-in flag: the response parser only honors `fallback` content
|
|
2010
|
+
// blocks and `usage.iterations` when the current request opted into
|
|
2011
|
+
// server-side-fallback beta chain. Leaving `fallbacks` unset preserves
|
|
2012
|
+
// the pre-fallback stream shape on every event.
|
|
2013
|
+
const serverSideFallback = !!fallbacks?.length;
|
|
2014
|
+
type Block = (
|
|
2015
|
+
| ThinkingContent
|
|
2016
|
+
| RedactedThinkingContent
|
|
2017
|
+
| TextContent
|
|
2018
|
+
| AnthropicFallbackContent
|
|
2019
|
+
| (AnthropicServerToolContent & { [kStreamingPartialJson]?: string })
|
|
2020
|
+
| (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number })
|
|
2021
|
+
) & { [kStreamingBlockIndex]: number };
|
|
2022
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
|
|
2023
|
+
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
2024
|
+
const requestTimeoutMs =
|
|
2025
|
+
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
2026
|
+
const blocks = output.content as Block[];
|
|
2027
|
+
const finalizeStreamBlock = (block: Block, contentIndex: number): void => {
|
|
2028
|
+
if (block.type === "text") {
|
|
2029
|
+
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
2030
|
+
} else if (block.type === "thinking") {
|
|
2031
|
+
const unwrappedThinking = unwrapAnthropicThinkingEnvelope(block.thinking);
|
|
2032
|
+
if (unwrappedThinking !== undefined) {
|
|
2033
|
+
block.thinking = unwrappedThinking;
|
|
2034
|
+
block.thinkingSignature = undefined;
|
|
2035
|
+
}
|
|
2036
|
+
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
2037
|
+
} else if (block.type === "anthropicServerTool" && block.block.type === "server_tool_use") {
|
|
2038
|
+
const partialJson = block[kStreamingPartialJson];
|
|
2039
|
+
if (partialJson) {
|
|
2040
|
+
try {
|
|
2041
|
+
const input = parseJsonWithRepair(partialJson);
|
|
2042
|
+
if (isRecord(input)) {
|
|
2043
|
+
block.block.input = input;
|
|
2044
|
+
} else {
|
|
2045
|
+
reportAnthropicEnvelopeAnomaly("server_tool_use input is not a JSON object");
|
|
2046
|
+
}
|
|
2047
|
+
} catch (parseError) {
|
|
2048
|
+
reportAnthropicEnvelopeAnomaly(
|
|
2049
|
+
`server_tool_use ${block.block.id} input is not valid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
clearStreamingPartialJson(block);
|
|
2054
|
+
} else if (block.type === "toolCall") {
|
|
2055
|
+
const finalJson =
|
|
2056
|
+
block[kStreamingPartialJson].length > 0
|
|
2057
|
+
? block[kStreamingPartialJson]
|
|
2058
|
+
: JSON.stringify(block.arguments ?? {});
|
|
2059
|
+
try {
|
|
2060
|
+
block.arguments = parseJsonWithRepair(finalJson) as ToolCall["arguments"];
|
|
2061
|
+
} catch (parseError) {
|
|
2062
|
+
// Non-fatal: keep the best-effort arguments recovered by the throttled streaming
|
|
2063
|
+
// parser instead of failing the turn on malformed/truncated tool-argument JSON.
|
|
2064
|
+
reportAnthropicEnvelopeAnomaly(
|
|
2065
|
+
`tool_use ${block.id} arguments are not valid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
2066
|
+
);
|
|
2067
|
+
const recoveredKeys = Object.keys(block.arguments ?? {});
|
|
2068
|
+
if (recoveredKeys.length === 0) {
|
|
2069
|
+
const maxLen = 512;
|
|
2070
|
+
const truncatedJson =
|
|
2071
|
+
finalJson.length <= maxLen
|
|
2072
|
+
? finalJson
|
|
2073
|
+
: `${finalJson.slice(0, maxLen)}… [truncated ${finalJson.length - maxLen} chars]`;
|
|
2074
|
+
block.arguments = {
|
|
2075
|
+
__parseError: parseError instanceof Error ? parseError.message : String(parseError),
|
|
2076
|
+
__rawJson: truncatedJson,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
clearStreamingPartialJson(block);
|
|
2081
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
stream.push({ type: "start", partial: output });
|
|
2085
|
+
// Retry loop for transient errors from the stream.
|
|
2086
|
+
// Provider-level transport/rate-limit failures: only before any streamed content starts.
|
|
2087
|
+
// Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
|
|
2088
|
+
let providerRetryAttempt = 0;
|
|
2089
|
+
const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
|
|
2090
|
+
"Anthropic stream timed out while waiting for the first event",
|
|
2091
|
+
);
|
|
2092
|
+
const idleTimeoutAbortError = new AIError.StreamTimeoutError(
|
|
2093
|
+
"Anthropic stream stalled while waiting for the next event",
|
|
2094
|
+
);
|
|
2095
|
+
while (true) {
|
|
2096
|
+
activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
2097
|
+
const { requestSignal } = activeAbortTracker;
|
|
2098
|
+
// The provider loop owns retries: pin the client's internal retry loop
|
|
2099
|
+
// to zero even when no watchdog timeout is configured (the helper only
|
|
2100
|
+
// pins it alongside a timeout; a client retry budget of 5 would otherwise
|
|
2101
|
+
// multiply with PROVIDER_MAX_RETRIES into up to 66 wire attempts).
|
|
2102
|
+
// Injected SDK clients (`options.client`) bypass the client-level
|
|
2103
|
+
// `anthropic-beta` construction below, so any `output_config.effort` the
|
|
2104
|
+
// body carries — the adaptive-only thinking-off / forced-tool pins and
|
|
2105
|
+
// enabled-effort turns alike — would reach Anthropic without the required
|
|
2106
|
+
// `effort-2025-11-24` beta and 400. `create()` accepts per-request headers
|
|
2107
|
+
// (already used for the gateway web-search header), so merge the beta with
|
|
2108
|
+
// any caller-provided `anthropic-beta` (deduped) and attach it there. Vertex
|
|
2109
|
+
// never carries the effort field (dropped in buildParams), so it is unaffected.
|
|
2110
|
+
const injectedClientEffortHeaders =
|
|
2111
|
+
options?.client !== undefined &&
|
|
2112
|
+
(params.output_config as AnthropicOutputConfig | undefined)?.effort !== undefined
|
|
2113
|
+
? mergeAnthropicBetaHeader(mergedCallerHeaders, effortBeta)
|
|
2114
|
+
: undefined;
|
|
2115
|
+
const perRequestHeaders =
|
|
2116
|
+
umansGatewayWebSearchHeader || injectedClientEffortHeaders
|
|
2117
|
+
? { ...umansGatewayWebSearchHeader, ...injectedClientEffortHeaders }
|
|
2118
|
+
: undefined;
|
|
2119
|
+
const requestOptions = {
|
|
2120
|
+
...createSdkStreamRequestOptions(requestSignal, requestTimeoutMs),
|
|
2121
|
+
maxRetries: 0,
|
|
2122
|
+
...(perRequestHeaders ? { headers: perRequestHeaders } : {}),
|
|
2123
|
+
};
|
|
2124
|
+
const anthropicRequest: unknown =
|
|
2125
|
+
isOAuthToken && client.beta
|
|
2126
|
+
? client.beta.messages.create({ ...params, stream: true }, requestOptions)
|
|
2127
|
+
: client.messages.create({ ...params, stream: true }, requestOptions);
|
|
2128
|
+
let streamedReplayUnsafeContent = false;
|
|
2129
|
+
|
|
2130
|
+
try {
|
|
2131
|
+
let requestTimeout: NodeJS.Timeout | undefined;
|
|
2132
|
+
if (requestTimeoutMs !== undefined) {
|
|
2133
|
+
requestTimeout = setTimeout(
|
|
2134
|
+
() => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
2135
|
+
requestTimeoutMs,
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
let anthropicStream: AsyncIterable<AnthropicStreamEvent>;
|
|
2139
|
+
let response: Response;
|
|
2140
|
+
let requestId: string | null;
|
|
2141
|
+
let recordsRawSseEvents: boolean;
|
|
2142
|
+
try {
|
|
2143
|
+
({
|
|
2144
|
+
events: anthropicStream,
|
|
2145
|
+
response,
|
|
2146
|
+
requestId,
|
|
2147
|
+
recordsRawSseEvents,
|
|
2148
|
+
} = await getAnthropicStreamResponse(anthropicRequest, requestSignal, rawSseObserver));
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (error instanceof AnthropicConnectionTimeoutError && !activeAbortTracker.wasCallerAbort()) {
|
|
2151
|
+
throw firstEventTimeoutAbortError;
|
|
2152
|
+
}
|
|
2153
|
+
throw error;
|
|
2154
|
+
} finally {
|
|
2155
|
+
if (requestTimeout !== undefined) clearTimeout(requestTimeout);
|
|
2156
|
+
}
|
|
2157
|
+
await notifyProviderResponse(options, response, model, requestId);
|
|
2158
|
+
let sawEvent = false;
|
|
2159
|
+
let sawMessageStart = false;
|
|
2160
|
+
let sawTerminalEnvelope = false;
|
|
2161
|
+
let sawMessageStop = false;
|
|
2162
|
+
// Set when a duplicate message_start splices a second envelope onto
|
|
2163
|
+
// the stream; closed indexes then refuse to reopen so replayed
|
|
2164
|
+
// content cannot duplicate (see content_block_start guard).
|
|
2165
|
+
let sawSplicedEnvelope = false;
|
|
2166
|
+
const closedBlockIndexes = new Set<number>();
|
|
2167
|
+
const openBlocks = new Map<
|
|
2168
|
+
number,
|
|
2169
|
+
{
|
|
2170
|
+
contentIndex: number;
|
|
2171
|
+
kind:
|
|
2172
|
+
| "text"
|
|
2173
|
+
| "thinking"
|
|
2174
|
+
| "redactedThinking"
|
|
2175
|
+
| "fallback"
|
|
2176
|
+
| "anthropicServerTool"
|
|
2177
|
+
| "toolCall"
|
|
2178
|
+
| "ignored";
|
|
2179
|
+
}
|
|
2180
|
+
>();
|
|
2181
|
+
|
|
2182
|
+
// Pings keep the idle deadline alive once content is flowing (Anthropic
|
|
2183
|
+
// bridges legitimate generation gaps with keepalives), but only within a
|
|
2184
|
+
// bounded window: a wedged upstream that pings forever while the model
|
|
2185
|
+
// produces nothing must still trip the idle watchdog, otherwise an
|
|
2186
|
+
// active tool-call stream hangs unrecoverably with no retry (#4900).
|
|
2187
|
+
// A ping before message_start must not consume the first-event watchdog
|
|
2188
|
+
// either: it would flip the (retryable) pre-content stall classification
|
|
2189
|
+
// into a terminal mid-stream idle timeout.
|
|
2190
|
+
let sawNonPingEvent = false;
|
|
2191
|
+
let lastNonPingProgressAtMs = 0;
|
|
2192
|
+
const pingProgressCapMs =
|
|
2193
|
+
idleTimeoutMs !== undefined && idleTimeoutMs > 0
|
|
2194
|
+
? idleTimeoutMs * PING_PROGRESS_MAX_IDLE_MULTIPLIER
|
|
2195
|
+
: undefined;
|
|
2196
|
+
const timedAnthropicStream = iterateWithIdleTimeout(anthropicStream, {
|
|
2197
|
+
idleTimeoutMs,
|
|
2198
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
2199
|
+
errorMessage: idleTimeoutAbortError.message,
|
|
2200
|
+
firstItemErrorMessage: firstEventTimeoutAbortError.message,
|
|
2201
|
+
onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
|
|
2202
|
+
onFirstItemTimeout: () => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
2203
|
+
abortSignal: options?.signal,
|
|
2204
|
+
isProgressItem: item => {
|
|
2205
|
+
if ((item as AnthropicStreamEvent).type === "ping") {
|
|
2206
|
+
if (!sawNonPingEvent) return false;
|
|
2207
|
+
if (pingProgressCapMs === undefined) return true;
|
|
2208
|
+
return Date.now() - lastNonPingProgressAtMs < pingProgressCapMs;
|
|
2209
|
+
}
|
|
2210
|
+
sawNonPingEvent = true;
|
|
2211
|
+
lastNonPingProgressAtMs = Date.now();
|
|
2212
|
+
return true;
|
|
2213
|
+
},
|
|
2214
|
+
});
|
|
2215
|
+
const observedAnthropicStream =
|
|
2216
|
+
rawSseObserver && !recordsRawSseEvents
|
|
2217
|
+
? observeDecodedAnthropicSdkEvents(timedAnthropicStream, rawSseObserver)
|
|
2218
|
+
: timedAnthropicStream;
|
|
2219
|
+
for await (const event of observedAnthropicStream) {
|
|
2220
|
+
sawEvent = true;
|
|
2221
|
+
|
|
2222
|
+
if (event.type === "message_start") {
|
|
2223
|
+
if (sawMessageStart) {
|
|
2224
|
+
// Transparent reconnects can splice a fresh envelope onto the same
|
|
2225
|
+
// stream; keep the original message but surface the anomaly. Events
|
|
2226
|
+
// for blocks still open from the first envelope continue to apply,
|
|
2227
|
+
// but replayed blocks are dropped below (see closedBlockIndexes).
|
|
2228
|
+
reportAnthropicEnvelopeAnomaly("duplicate message_start event");
|
|
2229
|
+
sawSplicedEnvelope = true;
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
sawMessageStart = true;
|
|
2233
|
+
const startMessage = event.message;
|
|
2234
|
+
if (startMessage?.id) output.responseId = startMessage.id;
|
|
2235
|
+
const startUsage = startMessage?.usage;
|
|
2236
|
+
if (startUsage) {
|
|
2237
|
+
applyAnthropicUsageExtras(output.usage, startUsage);
|
|
2238
|
+
output.usage.input = startUsage.input_tokens || 0;
|
|
2239
|
+
output.usage.output = startUsage.output_tokens || 0;
|
|
2240
|
+
output.usage.cacheRead = startUsage.cache_read_input_tokens || 0;
|
|
2241
|
+
output.usage.cacheWrite = startUsage.cache_creation_input_tokens || 0;
|
|
2242
|
+
output.usage.totalTokens =
|
|
2243
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
2244
|
+
if (serverSideFallback) {
|
|
2245
|
+
const served = fallbackServedModelFromUsage(startUsage);
|
|
2246
|
+
if (served) output.model = served;
|
|
2247
|
+
if (!calculateFallbackTurnCost(model, output.usage, startUsage)) {
|
|
2248
|
+
calculateCost(model, output.usage);
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
calculateCost(model, output.usage);
|
|
2252
|
+
}
|
|
2253
|
+
} else {
|
|
2254
|
+
reportAnthropicEnvelopeAnomaly("message_start missing usage");
|
|
2255
|
+
}
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
if (!sawMessageStart) {
|
|
2260
|
+
if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
throw new AIError.AnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (event.type === "content_block_start") {
|
|
2267
|
+
if (sawTerminalEnvelope) {
|
|
2268
|
+
reportAnthropicEnvelopeAnomaly(`received ${event.type} after terminal stop signal`);
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
if (openBlocks.has(event.index)) {
|
|
2272
|
+
reportAnthropicEnvelopeAnomaly(`duplicate content_block_start index ${event.index}`);
|
|
2273
|
+
continue;
|
|
2274
|
+
}
|
|
2275
|
+
if (sawSplicedEnvelope && closedBlockIndexes.has(event.index)) {
|
|
2276
|
+
// A spliced envelope replaying an index this stream already
|
|
2277
|
+
// completed would append duplicate text/tool calls; consume its
|
|
2278
|
+
// events silently instead.
|
|
2279
|
+
reportAnthropicEnvelopeAnomaly(
|
|
2280
|
+
`replayed content_block_start index ${event.index} after duplicate message_start`,
|
|
2281
|
+
);
|
|
2282
|
+
openBlocks.set(event.index, { contentIndex: -1, kind: "ignored" });
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
if (!event.content_block?.type) {
|
|
2286
|
+
reportAnthropicEnvelopeAnomaly("content_block_start missing content_block payload");
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
2290
|
+
if (event.content_block.type === "fallback") {
|
|
2291
|
+
// Fallback boundary is only meaningful when the request
|
|
2292
|
+
// opted into the beta chain — silently drop otherwise so
|
|
2293
|
+
// unopted-in sessions never see the block persisted or
|
|
2294
|
+
// influence downstream converters.
|
|
2295
|
+
const fallback = parseAnthropicFallbackWireBlock(event.content_block);
|
|
2296
|
+
if (!serverSideFallback || !fallback) {
|
|
2297
|
+
if (!fallback) {
|
|
2298
|
+
reportAnthropicEnvelopeAnomaly("fallback content_block missing model refs");
|
|
2299
|
+
}
|
|
2300
|
+
openBlocks.set(event.index, { contentIndex: -1, kind: "ignored" });
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
const block: Block = { ...fallback, [kStreamingBlockIndex]: event.index };
|
|
2304
|
+
output.content.push(block);
|
|
2305
|
+
openBlocks.set(event.index, {
|
|
2306
|
+
contentIndex: output.content.length - 1,
|
|
2307
|
+
kind: "fallback",
|
|
2308
|
+
});
|
|
2309
|
+
// A fallback content block is the mid-stream signal that a
|
|
2310
|
+
// classifier block on the primary was retried on the
|
|
2311
|
+
// fallback model. Adopt the served id immediately so
|
|
2312
|
+
// pricing decisions downstream (final usage.iterations may
|
|
2313
|
+
// arrive before/after) see the right model.
|
|
2314
|
+
output.model = fallback.to.model;
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
if (event.content_block.type === "text") {
|
|
2318
|
+
streamedReplayUnsafeContent = true;
|
|
2319
|
+
const block: Block = {
|
|
2320
|
+
type: "text",
|
|
2321
|
+
text: "",
|
|
2322
|
+
[kStreamingBlockIndex]: event.index,
|
|
2323
|
+
};
|
|
2324
|
+
output.content.push(block);
|
|
2325
|
+
const contentIndex = output.content.length - 1;
|
|
2326
|
+
openBlocks.set(event.index, { contentIndex, kind: "text" });
|
|
2327
|
+
stream.push({
|
|
2328
|
+
type: "text_start",
|
|
2329
|
+
contentIndex,
|
|
2330
|
+
partial: output,
|
|
2331
|
+
});
|
|
2332
|
+
} else if (event.content_block.type === "thinking") {
|
|
2333
|
+
streamedReplayUnsafeContent = true;
|
|
2334
|
+
const block: Block = {
|
|
2335
|
+
type: "thinking",
|
|
2336
|
+
thinking: "",
|
|
2337
|
+
thinkingSignature: "",
|
|
2338
|
+
[kStreamingBlockIndex]: event.index,
|
|
2339
|
+
};
|
|
2340
|
+
output.content.push(block);
|
|
2341
|
+
const contentIndex = output.content.length - 1;
|
|
2342
|
+
openBlocks.set(event.index, { contentIndex, kind: "thinking" });
|
|
2343
|
+
stream.push({
|
|
2344
|
+
type: "thinking_start",
|
|
2345
|
+
contentIndex,
|
|
2346
|
+
partial: output,
|
|
2347
|
+
});
|
|
2348
|
+
} else if (event.content_block.type === "redacted_thinking") {
|
|
2349
|
+
streamedReplayUnsafeContent = true;
|
|
2350
|
+
const block: Block = {
|
|
2351
|
+
type: "redactedThinking",
|
|
2352
|
+
data: event.content_block.data,
|
|
2353
|
+
[kStreamingBlockIndex]: event.index,
|
|
2354
|
+
};
|
|
2355
|
+
output.content.push(block);
|
|
2356
|
+
openBlocks.set(event.index, {
|
|
2357
|
+
contentIndex: output.content.length - 1,
|
|
2358
|
+
kind: "redactedThinking",
|
|
2359
|
+
});
|
|
2360
|
+
} else if (
|
|
2361
|
+
isAnthropicWebSearchHistoryBlock(event.content_block) &&
|
|
2362
|
+
umansGatewayWebSearchHeader === undefined
|
|
2363
|
+
) {
|
|
2364
|
+
streamedReplayUnsafeContent = true;
|
|
2365
|
+
const block: Block = {
|
|
2366
|
+
type: "anthropicServerTool",
|
|
2367
|
+
block: { ...event.content_block },
|
|
2368
|
+
[kStreamingPartialJson]: "",
|
|
2369
|
+
[kStreamingBlockIndex]: event.index,
|
|
2370
|
+
};
|
|
2371
|
+
output.content.push(block);
|
|
2372
|
+
openBlocks.set(event.index, {
|
|
2373
|
+
contentIndex: output.content.length - 1,
|
|
2374
|
+
kind: "anthropicServerTool",
|
|
2375
|
+
});
|
|
2376
|
+
} else if (event.content_block.type === "tool_use") {
|
|
2377
|
+
streamedReplayUnsafeContent = true;
|
|
2378
|
+
const block: Block = {
|
|
2379
|
+
type: "toolCall",
|
|
2380
|
+
id: event.content_block.id,
|
|
2381
|
+
name: decodeAnthropicToolName(
|
|
2382
|
+
event.content_block.name,
|
|
2383
|
+
isOAuthToken,
|
|
2384
|
+
model.compat.escapeBuiltinToolNames,
|
|
2385
|
+
),
|
|
2386
|
+
arguments: event.content_block.input ?? {},
|
|
2387
|
+
[kStreamingPartialJson]: "",
|
|
2388
|
+
[kStreamingBlockIndex]: event.index,
|
|
2389
|
+
};
|
|
2390
|
+
output.content.push(block);
|
|
2391
|
+
const contentIndex = output.content.length - 1;
|
|
2392
|
+
openBlocks.set(event.index, { contentIndex, kind: "toolCall" });
|
|
2393
|
+
stream.push({
|
|
2394
|
+
type: "toolcall_start",
|
|
2395
|
+
contentIndex,
|
|
2396
|
+
partial: output,
|
|
2397
|
+
});
|
|
2398
|
+
} else {
|
|
2399
|
+
openBlocks.set(event.index, { contentIndex: -1, kind: "ignored" });
|
|
2400
|
+
}
|
|
2401
|
+
} else if (event.type === "content_block_delta") {
|
|
2402
|
+
if (sawTerminalEnvelope) {
|
|
2403
|
+
reportAnthropicEnvelopeAnomaly(`received ${event.type} after terminal stop signal`);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const openBlock = openBlocks.get(event.index);
|
|
2407
|
+
if (!openBlock) {
|
|
2408
|
+
reportAnthropicEnvelopeAnomaly(
|
|
2409
|
+
`received content_block_delta for unopened index ${event.index}`,
|
|
2410
|
+
);
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (openBlock.kind === "ignored") continue;
|
|
2414
|
+
if (!event.delta?.type) {
|
|
2415
|
+
reportAnthropicEnvelopeAnomaly("content_block_delta missing delta payload");
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
const block = blocks[openBlock.contentIndex];
|
|
2419
|
+
if (event.delta.type === "text_delta") {
|
|
2420
|
+
if (openBlock.kind !== "text" || block?.type !== "text") {
|
|
2421
|
+
reportAnthropicEnvelopeAnomaly(`received text_delta for ${openBlock.kind} block`);
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
streamedReplayUnsafeContent = true;
|
|
2425
|
+
block.text += event.delta.text;
|
|
2426
|
+
stream.push({
|
|
2427
|
+
type: "text_delta",
|
|
2428
|
+
contentIndex: openBlock.contentIndex,
|
|
2429
|
+
delta: event.delta.text,
|
|
2430
|
+
partial: output,
|
|
2431
|
+
});
|
|
2432
|
+
} else if (event.delta.type === "thinking_delta") {
|
|
2433
|
+
if (openBlock.kind !== "thinking" || block?.type !== "thinking") {
|
|
2434
|
+
reportAnthropicEnvelopeAnomaly(`received thinking_delta for ${openBlock.kind} block`);
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
streamedReplayUnsafeContent = true;
|
|
2438
|
+
block.thinking += event.delta.thinking;
|
|
2439
|
+
stream.push({
|
|
2440
|
+
type: "thinking_delta",
|
|
2441
|
+
contentIndex: openBlock.contentIndex,
|
|
2442
|
+
delta: event.delta.thinking,
|
|
2443
|
+
partial: output,
|
|
2444
|
+
});
|
|
2445
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
2446
|
+
if (
|
|
2447
|
+
openBlock.kind === "anthropicServerTool" &&
|
|
2448
|
+
block?.type === "anthropicServerTool" &&
|
|
2449
|
+
block.block.type === "server_tool_use"
|
|
2450
|
+
) {
|
|
2451
|
+
block[kStreamingPartialJson] =
|
|
2452
|
+
(block[kStreamingPartialJson] ?? "") + event.delta.partial_json;
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2455
|
+
if (openBlock.kind !== "toolCall" || block?.type !== "toolCall") {
|
|
2456
|
+
reportAnthropicEnvelopeAnomaly(`received input_json_delta for ${openBlock.kind} block`);
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
streamedReplayUnsafeContent = true;
|
|
2460
|
+
block[kStreamingPartialJson] += event.delta.partial_json;
|
|
2461
|
+
const throttled = parseStreamingJsonThrottled(
|
|
2462
|
+
block[kStreamingPartialJson],
|
|
2463
|
+
block[kStreamingLastParseLen] ?? 0,
|
|
2464
|
+
);
|
|
2465
|
+
if (throttled) {
|
|
2466
|
+
block.arguments = throttled.value;
|
|
2467
|
+
block[kStreamingLastParseLen] = throttled.parsedLen;
|
|
2468
|
+
}
|
|
2469
|
+
stream.push({
|
|
2470
|
+
type: "toolcall_delta",
|
|
2471
|
+
contentIndex: openBlock.contentIndex,
|
|
2472
|
+
delta: event.delta.partial_json,
|
|
2473
|
+
partial: output,
|
|
2474
|
+
});
|
|
2475
|
+
} else if (event.delta.type === "signature_delta") {
|
|
2476
|
+
if (openBlock.kind !== "thinking" || block?.type !== "thinking") {
|
|
2477
|
+
reportAnthropicEnvelopeAnomaly(`received signature_delta for ${openBlock.kind} block`);
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
streamedReplayUnsafeContent = true;
|
|
2481
|
+
block.thinkingSignature = block.thinkingSignature || "";
|
|
2482
|
+
block.thinkingSignature += event.delta.signature;
|
|
2483
|
+
}
|
|
2484
|
+
} else if (event.type === "content_block_stop") {
|
|
2485
|
+
if (sawTerminalEnvelope) {
|
|
2486
|
+
reportAnthropicEnvelopeAnomaly(`received ${event.type} after terminal stop signal`);
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const openBlock = openBlocks.get(event.index);
|
|
2490
|
+
if (!openBlock) {
|
|
2491
|
+
reportAnthropicEnvelopeAnomaly(`received content_block_stop for unopened index ${event.index}`);
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
if (openBlock.kind === "ignored") {
|
|
2495
|
+
openBlocks.delete(event.index);
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
const block = blocks[openBlock.contentIndex];
|
|
2499
|
+
if (!block || block.type !== openBlock.kind) {
|
|
2500
|
+
reportAnthropicEnvelopeAnomaly(`content_block_stop kind mismatch for index ${event.index}`);
|
|
2501
|
+
openBlocks.delete(event.index);
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
openBlocks.delete(event.index);
|
|
2505
|
+
closedBlockIndexes.add(event.index);
|
|
2506
|
+
finalizeStreamBlock(block, openBlock.contentIndex);
|
|
2507
|
+
} else if (event.type === "message_delta") {
|
|
2508
|
+
if (sawTerminalEnvelope) {
|
|
2509
|
+
// A spliced reconnect's second envelope must not overwrite the
|
|
2510
|
+
// completed message's stop reason or usage.
|
|
2511
|
+
reportAnthropicEnvelopeAnomaly("received message_delta after terminal stop signal");
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
const delta = event.delta;
|
|
2515
|
+
const rawStopReason = delta?.stop_reason;
|
|
2516
|
+
if (rawStopReason) {
|
|
2517
|
+
output.stopReason = mapStopReason(rawStopReason);
|
|
2518
|
+
sawTerminalEnvelope = true;
|
|
2519
|
+
}
|
|
2520
|
+
if (output.stopReason === "error") {
|
|
2521
|
+
const stopDetails = delta?.stop_details;
|
|
2522
|
+
output.stopDetails = stopDetails ?? (rawStopReason ? { type: rawStopReason } : null);
|
|
2523
|
+
if (stopDetails?.type === "refusal") {
|
|
2524
|
+
const explanation = stopDetails.explanation?.trim();
|
|
2525
|
+
const category = stopDetails.category;
|
|
2526
|
+
const label = category ? `Refusal (${category})` : "Refusal";
|
|
2527
|
+
output.errorMessage = explanation ? `${label}: ${explanation}` : label;
|
|
2528
|
+
} else if (!output.errorMessage) {
|
|
2529
|
+
// Anthropic flagged an error-class stop (refusal / sensitive) without
|
|
2530
|
+
// populating stop_details. Surface the raw reason instead of falling
|
|
2531
|
+
// through to the generic "unknown error" string when we throw below.
|
|
2532
|
+
output.errorMessage =
|
|
2533
|
+
rawStopReason === "refusal"
|
|
2534
|
+
? "Refusal (no details provided)"
|
|
2535
|
+
: rawStopReason === "sensitive"
|
|
2536
|
+
? "Content flagged by safety filters"
|
|
2537
|
+
: `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
const deltaUsage = event.usage;
|
|
2541
|
+
if (deltaUsage) {
|
|
2542
|
+
if (deltaUsage.input_tokens != null) {
|
|
2543
|
+
output.usage.input = deltaUsage.input_tokens;
|
|
2544
|
+
}
|
|
2545
|
+
if (deltaUsage.output_tokens != null) {
|
|
2546
|
+
output.usage.output = deltaUsage.output_tokens;
|
|
2547
|
+
}
|
|
2548
|
+
if (deltaUsage.cache_read_input_tokens != null) {
|
|
2549
|
+
output.usage.cacheRead = deltaUsage.cache_read_input_tokens;
|
|
2550
|
+
}
|
|
2551
|
+
if (deltaUsage.cache_creation_input_tokens != null) {
|
|
2552
|
+
output.usage.cacheWrite = deltaUsage.cache_creation_input_tokens;
|
|
2553
|
+
}
|
|
2554
|
+
applyAnthropicUsageExtras(output.usage, deltaUsage);
|
|
2555
|
+
output.usage.totalTokens =
|
|
2556
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
2557
|
+
if (serverSideFallback) {
|
|
2558
|
+
const served = fallbackServedModelFromUsage(deltaUsage);
|
|
2559
|
+
if (served) output.model = served;
|
|
2560
|
+
if (!calculateFallbackTurnCost(model, output.usage, deltaUsage)) {
|
|
2561
|
+
calculateCost(model, output.usage);
|
|
2562
|
+
}
|
|
2563
|
+
} else {
|
|
2564
|
+
calculateCost(model, output.usage);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
} else if (event.type === "message_stop") {
|
|
2568
|
+
sawTerminalEnvelope = true;
|
|
2569
|
+
sawMessageStop = true;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
|
|
2574
|
+
if (firstEventTimeoutError) {
|
|
2575
|
+
throw firstEventTimeoutError;
|
|
2576
|
+
}
|
|
2577
|
+
if (activeAbortTracker.wasCallerAbort()) {
|
|
2578
|
+
throw new AIError.AbortError();
|
|
2579
|
+
}
|
|
2580
|
+
if (!sawEvent || !sawMessageStart) {
|
|
2581
|
+
throw new AIError.AnthropicStreamEnvelopeError("stream ended before message_start");
|
|
2582
|
+
}
|
|
2583
|
+
if (!sawTerminalEnvelope) {
|
|
2584
|
+
// Neither a message_delta stop_reason nor message_stop arrived: the
|
|
2585
|
+
// connection died mid-generation. Finalizing the partial message as
|
|
2586
|
+
// a clean "stop" would make the agent loop treat the truncated turn
|
|
2587
|
+
// as complete (silent mid-sentence halt), so fail the turn. The
|
|
2588
|
+
// envelope error is transparently retried before replay-unsafe
|
|
2589
|
+
// content streams; afterwards it surfaces as an error turn whose
|
|
2590
|
+
// complete tool calls the agent loop salvages
|
|
2591
|
+
// (`recoverTransientErrorToolTurn` recognizes the envelope-error
|
|
2592
|
+
// text and `retainCompletedToolCalls` drops half-streamed calls).
|
|
2593
|
+
throw new AIError.AnthropicStreamEnvelopeError("stream ended before message_stop");
|
|
2594
|
+
}
|
|
2595
|
+
if (!sawMessageStop) {
|
|
2596
|
+
// A stop_reason arrived via message_delta, so generation finished;
|
|
2597
|
+
// only the trailing message_stop frame is missing (non-conforming
|
|
2598
|
+
// gateway). Degrade to best-effort instead of discarding the turn.
|
|
2599
|
+
reportAnthropicEnvelopeAnomaly("stream ended before message_stop");
|
|
2600
|
+
}
|
|
2601
|
+
if (openBlocks.size > 0) {
|
|
2602
|
+
for (const [openIndex, openBlock] of openBlocks) {
|
|
2603
|
+
reportAnthropicEnvelopeAnomaly(
|
|
2604
|
+
`stream ended with an unterminated ${openBlock.kind} block at index ${openIndex}`,
|
|
2605
|
+
);
|
|
2606
|
+
if (openBlock.kind === "ignored" || openBlock.contentIndex < 0) continue;
|
|
2607
|
+
const danglingBlock = blocks[openBlock.contentIndex];
|
|
2608
|
+
if (danglingBlock) finalizeStreamBlock(danglingBlock, openBlock.contentIndex);
|
|
2609
|
+
}
|
|
2610
|
+
openBlocks.clear();
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
2614
|
+
throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
|
|
2615
|
+
provider: model.provider,
|
|
2616
|
+
kind: "output",
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
break;
|
|
2620
|
+
} catch (streamError) {
|
|
2621
|
+
const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
|
|
2622
|
+
if (
|
|
2623
|
+
!disableStrictTools &&
|
|
2624
|
+
firstTokenTime === undefined &&
|
|
2625
|
+
hasStrictAnthropicTools(params) &&
|
|
2626
|
+
AIError.isGrammarError(streamFailure)
|
|
2627
|
+
) {
|
|
2628
|
+
// Log-only: the retried turn must not carry an errorMessage on
|
|
2629
|
+
// success (consumers treat its presence as failure).
|
|
2630
|
+
logger.warn("anthropic: strict tools rejected, retrying without strict tools", {
|
|
2631
|
+
model: model.id,
|
|
2632
|
+
error: await finalizeErrorMessage(streamFailure, rawRequestDump),
|
|
2633
|
+
});
|
|
2634
|
+
if (providerSessionState) {
|
|
2635
|
+
providerSessionState.strictToolsDisabled = true;
|
|
2636
|
+
}
|
|
2637
|
+
disableStrictTools = true;
|
|
2638
|
+
params = await prepareParams();
|
|
2639
|
+
providerRetryAttempt = 0;
|
|
2640
|
+
output.content.length = 0;
|
|
2641
|
+
output.model = model.id;
|
|
2642
|
+
output.responseId = undefined;
|
|
2643
|
+
output.errorMessage = undefined;
|
|
2644
|
+
output.providerPayload = undefined;
|
|
2645
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
2646
|
+
output.stopReason = "stop";
|
|
2647
|
+
firstTokenTime = undefined;
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
if (
|
|
2651
|
+
!forceDemoteUnsignedThinking &&
|
|
2652
|
+
firstTokenTime === undefined &&
|
|
2653
|
+
!streamedReplayUnsafeContent &&
|
|
2654
|
+
isInvalidThinkingSignatureError(
|
|
2655
|
+
streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2656
|
+
)
|
|
2657
|
+
) {
|
|
2658
|
+
logger.warn(
|
|
2659
|
+
"anthropic: signing proxy detected (Invalid signature in thinking block), demoting unsigned thinking and retrying",
|
|
2660
|
+
{
|
|
2661
|
+
provider: model.provider,
|
|
2662
|
+
model: model.id,
|
|
2663
|
+
baseUrl,
|
|
2664
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2665
|
+
},
|
|
2666
|
+
);
|
|
2667
|
+
if (providerSessionState) {
|
|
2668
|
+
providerSessionState.replayUnsignedThinkingDisabled = true;
|
|
2669
|
+
}
|
|
2670
|
+
forceDemoteUnsignedThinking = true;
|
|
2671
|
+
params = await prepareParams();
|
|
2672
|
+
providerRetryAttempt = 0;
|
|
2673
|
+
output.content.length = 0;
|
|
2674
|
+
output.model = model.id;
|
|
2675
|
+
output.responseId = undefined;
|
|
2676
|
+
output.errorMessage = undefined;
|
|
2677
|
+
output.providerPayload = undefined;
|
|
2678
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
2679
|
+
output.stopReason = "stop";
|
|
2680
|
+
firstTokenTime = undefined;
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
if (
|
|
2684
|
+
!dropFastMode &&
|
|
2685
|
+
model.provider === "anthropic" &&
|
|
2686
|
+
options?.serviceTier === "priority" &&
|
|
2687
|
+
firstTokenTime === undefined &&
|
|
2688
|
+
AIError.isFastModeUnsupported(streamFailure)
|
|
2689
|
+
) {
|
|
2690
|
+
logger.debug("anthropic: fast mode unsupported, retrying without speed", {
|
|
2691
|
+
model: model.id,
|
|
2692
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2693
|
+
});
|
|
2694
|
+
if (providerSessionState) {
|
|
2695
|
+
providerSessionState.fastModeDisabled = true;
|
|
2696
|
+
}
|
|
2697
|
+
dropFastMode = true;
|
|
2698
|
+
params = await prepareParams();
|
|
2699
|
+
providerRetryAttempt = 0;
|
|
2700
|
+
output.content.length = 0;
|
|
2701
|
+
output.model = model.id;
|
|
2702
|
+
output.responseId = undefined;
|
|
2703
|
+
output.errorMessage = undefined;
|
|
2704
|
+
output.providerPayload = undefined;
|
|
2705
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
2706
|
+
output.stopReason = "stop";
|
|
2707
|
+
firstTokenTime = undefined;
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
const isTransientEnvelopeFailure =
|
|
2711
|
+
AIError.isTransientStreamParseError(streamFailure) || AIError.isStreamEnvelopeError(streamFailure);
|
|
2712
|
+
const isLocalIdleTimeout =
|
|
2713
|
+
streamFailure === idleTimeoutAbortError ||
|
|
2714
|
+
(streamFailure instanceof Error && streamFailure.message === idleTimeoutAbortError.message);
|
|
2715
|
+
const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
|
|
2716
|
+
const canRetryProviderFailure =
|
|
2717
|
+
!isLocalIdleTimeout &&
|
|
2718
|
+
firstTokenTime === undefined &&
|
|
2719
|
+
!streamedReplayUnsafeContent &&
|
|
2720
|
+
isProviderRetryableError(streamFailure, model.provider);
|
|
2721
|
+
if (
|
|
2722
|
+
activeAbortTracker.wasCallerAbort() ||
|
|
2723
|
+
providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
|
|
2724
|
+
(!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
|
|
2725
|
+
) {
|
|
2726
|
+
throw streamFailure;
|
|
2727
|
+
}
|
|
2728
|
+
providerRetryAttempt++;
|
|
2729
|
+
// Copilot's model-availability 400 is a per-request replica reroll, not
|
|
2730
|
+
// upstream backpressure — the exponential curve would just add dead
|
|
2731
|
+
// time to a coin flip that the next attempt is as likely to win.
|
|
2732
|
+
const backoffDelayMs = AIError.isCopilotTransientModelError(streamFailure)
|
|
2733
|
+
? COPILOT_MODEL_FLAP_RETRY_DELAY_MS
|
|
2734
|
+
: calculateAnthropicRetryDelayMs(providerRetryAttempt - 1);
|
|
2735
|
+
// Honor the server's retry hint (`retry-after-ms`/`retry-after`) on
|
|
2736
|
+
// 429/529-style failures: retrying sooner than the server asked is a
|
|
2737
|
+
// guaranteed failure that just burns the retry budget.
|
|
2738
|
+
const headerDelayMs = getRetryAfterMsFromHeaders(getHeadersFromError(streamFailure));
|
|
2739
|
+
// Bound the server-directed wait so a multi-hour `retry-after` cannot
|
|
2740
|
+
// park the provider stream before higher-level recovery runs. A non-positive cap
|
|
2741
|
+
// disables the bound; an over-cap hint surfaces the original error immediately.
|
|
2742
|
+
const maxRetryDelayMs = options?.maxRetryDelayMs ?? 60_000;
|
|
2743
|
+
if (headerDelayMs !== undefined && maxRetryDelayMs > 0 && headerDelayMs > maxRetryDelayMs) {
|
|
2744
|
+
throw streamFailure;
|
|
2745
|
+
}
|
|
2746
|
+
const delayMs = headerDelayMs !== undefined ? Math.max(headerDelayMs, backoffDelayMs) : backoffDelayMs;
|
|
2747
|
+
if (options?.providerRetryWait) {
|
|
2748
|
+
await options.providerRetryWait(delayMs, options.signal);
|
|
2749
|
+
} else {
|
|
2750
|
+
await scheduler.wait(delayMs, { signal: options?.signal });
|
|
2751
|
+
}
|
|
2752
|
+
output.content.length = 0;
|
|
2753
|
+
output.model = model.id;
|
|
2754
|
+
output.responseId = undefined;
|
|
2755
|
+
output.errorMessage = undefined;
|
|
2756
|
+
output.stopDetails = undefined;
|
|
2757
|
+
output.providerPayload = undefined;
|
|
2758
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
2759
|
+
output.stopReason = "stop";
|
|
2760
|
+
firstTokenTime = undefined;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
output.duration = performance.now() - startTime;
|
|
2764
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
2765
|
+
if (dropFastMode && model.provider === "anthropic" && options?.serviceTier === "priority") {
|
|
2766
|
+
output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
|
|
2767
|
+
}
|
|
2768
|
+
if (forceDemoteUnsignedThinking && model.compat.replayUnsignedThinking) {
|
|
2769
|
+
output.disabledFeatures = [...(output.disabledFeatures ?? []), "unsigned-thinking-replay"];
|
|
2770
|
+
}
|
|
2771
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
2772
|
+
stream.end();
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
for (const block of output.content) {
|
|
2775
|
+
if (block.type === "toolCall") clearStreamingPartialJson(block);
|
|
2776
|
+
}
|
|
2777
|
+
const result = await AIError.finalize(error, {
|
|
2778
|
+
api: model.api,
|
|
2779
|
+
provider: model.provider,
|
|
2780
|
+
abortTracker: activeAbortTracker,
|
|
2781
|
+
rawRequestDump,
|
|
2782
|
+
});
|
|
2783
|
+
output.stopReason = result.stopReason;
|
|
2784
|
+
output.errorStatus = result.status;
|
|
2785
|
+
output.errorId = result.id;
|
|
2786
|
+
output.errorMessage = maybeAddReplayUnsignedThinkingHint(model, result.message);
|
|
2787
|
+
output.duration = performance.now() - startTime;
|
|
2788
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
2789
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
2790
|
+
stream.end();
|
|
2791
|
+
}
|
|
2792
|
+
})();
|
|
2793
|
+
|
|
2794
|
+
return stream;
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
/**
|
|
2798
|
+
* Public entry: wrap the single-attempt streamer with bounded empty-completion
|
|
2799
|
+
* retries (a benign terminal stop carrying no content/usage would otherwise
|
|
2800
|
+
* stall the agent loop). The inner attempt keeps its own provider-failure retry
|
|
2801
|
+
* loop; this layer only re-issues a fresh request on an empty success. Shared
|
|
2802
|
+
* with the OpenAI-completions provider via `withEmptyCompletionRetry`.
|
|
2803
|
+
*/
|
|
2804
|
+
export const streamAnthropic: StreamFunction<"anthropic-messages"> = (model, context, options) =>
|
|
2805
|
+
withEmptyCompletionRetry(model, context, options, streamAnthropicOnce);
|
|
2806
|
+
|
|
2807
|
+
export type AnthropicSystemBlock = {
|
|
2808
|
+
type: "text";
|
|
2809
|
+
text: string;
|
|
2810
|
+
cache_control?: AnthropicCacheControl;
|
|
2811
|
+
};
|
|
2812
|
+
type SystemBlockOptions = {
|
|
2813
|
+
includeClaudeCodeInstruction?: boolean;
|
|
2814
|
+
extraInstructions?: string[];
|
|
2815
|
+
/** Text of the first user message — used as fingerprint seed for the billing header. */
|
|
2816
|
+
firstUserMessageText?: string;
|
|
2817
|
+
cacheControl?: AnthropicCacheControl;
|
|
2818
|
+
};
|
|
2819
|
+
|
|
2820
|
+
/**
|
|
2821
|
+
* Place system-block cache breakpoints that survive volatile project context.
|
|
2822
|
+
*
|
|
2823
|
+
* omp normally appends its project footer (cwd, date, workspace tree) after the
|
|
2824
|
+
* stable system prefix. When cwd is outside a single direct child repository,
|
|
2825
|
+
* an active-repo context block follows that footer. Caching up to the last three
|
|
2826
|
+
* eligible blocks therefore covers both layouts:
|
|
2827
|
+
*
|
|
2828
|
+
* - stable prefix, project footer
|
|
2829
|
+
* - stable prefix, project footer, active-repo context
|
|
2830
|
+
*
|
|
2831
|
+
* A footer change can then fall back to the stable-prefix entry instead of
|
|
2832
|
+
* re-writing the entire system cache (issue #7324).
|
|
2833
|
+
*
|
|
2834
|
+
* @returns breakpoints placed, capped by `maxBreakpoints`.
|
|
2835
|
+
*/
|
|
2836
|
+
function cacheSystemPrefixBreakpoints(
|
|
2837
|
+
blocks: AnthropicSystemBlock[],
|
|
2838
|
+
cacheControl: AnthropicCacheControl | undefined,
|
|
2839
|
+
maxBreakpoints: number,
|
|
2840
|
+
firstCacheableIndex: number,
|
|
2841
|
+
): number {
|
|
2842
|
+
if (!cacheControl || maxBreakpoints <= 0) return 0;
|
|
2843
|
+
let placed = 0;
|
|
2844
|
+
for (let index = blocks.length - 1; index >= firstCacheableIndex && placed < maxBreakpoints; index--) {
|
|
2845
|
+
if (blocks[index].cache_control != null) continue;
|
|
2846
|
+
blocks[index] = { ...blocks[index], cache_control: cloneAnthropicCacheControl(cacheControl) };
|
|
2847
|
+
placed++;
|
|
2848
|
+
}
|
|
2849
|
+
return placed;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/**
|
|
2853
|
+
* First system-block index that may carry a cache breakpoint. Skips the OAuth
|
|
2854
|
+
* cloak blocks that must stay uncached: the CC billing header (block 0, a
|
|
2855
|
+
* per-request fingerprint) and the Claude Code identity instruction (block 1).
|
|
2856
|
+
*/
|
|
2857
|
+
function firstCacheableSystemIndex(blocks: readonly AnthropicSystemBlock[]): number {
|
|
2858
|
+
let index = 0;
|
|
2859
|
+
if (blocks[index]?.text?.startsWith(CLAUDE_BILLING_HEADER_PREFIX)) index++;
|
|
2860
|
+
if (blocks[index]?.text === claudeCodeSystemInstruction) index++;
|
|
2861
|
+
return index;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
export function buildAnthropicSystemBlocks(
|
|
2865
|
+
systemPrompt: readonly string[] | undefined,
|
|
2866
|
+
options: SystemBlockOptions = {},
|
|
2867
|
+
): AnthropicSystemBlock[] | undefined {
|
|
2868
|
+
const { includeClaudeCodeInstruction = false, extraInstructions = [], firstUserMessageText, cacheControl } = options;
|
|
2869
|
+
const sanitizedPrompts = normalizeSystemPrompts(systemPrompt);
|
|
2870
|
+
const trimmedInstructions = extraInstructions.map(instruction => instruction.trim()).filter(Boolean);
|
|
2871
|
+
const hasBillingHeader = sanitizedPrompts.some(prompt => prompt.startsWith(CLAUDE_BILLING_HEADER_PREFIX));
|
|
2872
|
+
|
|
2873
|
+
if (includeClaudeCodeInstruction && !hasBillingHeader) {
|
|
2874
|
+
const blocks: AnthropicSystemBlock[] = [
|
|
2875
|
+
{ type: "text", text: createClaudeBillingHeader(firstUserMessageText ?? "") },
|
|
2876
|
+
{ type: "text", text: claudeCodeSystemInstruction },
|
|
2877
|
+
];
|
|
2878
|
+
|
|
2879
|
+
for (const instruction of trimmedInstructions) {
|
|
2880
|
+
blocks.push({ type: "text", text: instruction });
|
|
2881
|
+
}
|
|
2882
|
+
for (const prompt of sanitizedPrompts) {
|
|
2883
|
+
blocks.push({ type: "text", text: prompt });
|
|
2884
|
+
}
|
|
2885
|
+
cacheSystemPrefixBreakpoints(blocks, cacheControl, 3, firstCacheableSystemIndex(blocks));
|
|
2886
|
+
|
|
2887
|
+
return blocks;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
const blocks: AnthropicSystemBlock[] = [];
|
|
2891
|
+
for (const instruction of trimmedInstructions) {
|
|
2892
|
+
blocks.push({ type: "text", text: instruction });
|
|
2893
|
+
}
|
|
2894
|
+
for (const prompt of sanitizedPrompts) {
|
|
2895
|
+
blocks.push({ type: "text", text: prompt });
|
|
2896
|
+
}
|
|
2897
|
+
const lastIndex = blocks.length - 1;
|
|
2898
|
+
if (cacheControl && lastIndex >= 0 && blocks[lastIndex].cache_control == null) {
|
|
2899
|
+
blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cloneAnthropicCacheControl(cacheControl) };
|
|
2900
|
+
}
|
|
2901
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
export function normalizeExtraBetas(betas?: string[] | string): string[] {
|
|
2905
|
+
if (!betas) return [];
|
|
2906
|
+
const raw = Array.isArray(betas) ? betas : betas.split(",");
|
|
2907
|
+
return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult {
|
|
2911
|
+
const {
|
|
2912
|
+
model,
|
|
2913
|
+
apiKey,
|
|
2914
|
+
extraBetas = [],
|
|
2915
|
+
stream = true,
|
|
2916
|
+
interleavedThinking = true,
|
|
2917
|
+
headers,
|
|
2918
|
+
dynamicHeaders,
|
|
2919
|
+
hasTools = false,
|
|
2920
|
+
thinkingEnabled = false,
|
|
2921
|
+
isOAuth,
|
|
2922
|
+
maxRetryDelayMs,
|
|
2923
|
+
claudeCodeSessionId,
|
|
2924
|
+
disableStrictTools: disableStrictToolsOverride,
|
|
2925
|
+
} = args;
|
|
2926
|
+
const compat = model.compat;
|
|
2927
|
+
const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools;
|
|
2928
|
+
const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
|
|
2929
|
+
// Adaptive models (`supportsDisplay`) get native interleaved thinking on the
|
|
2930
|
+
// official API, so only non-official signing routes need the beta (#6717).
|
|
2931
|
+
// Two classifications feed the predicate: the effective URL, because Foundry
|
|
2932
|
+
// and provider overrides can reroute a model without rebuilding its
|
|
2933
|
+
// materialized compat, and non-official `compat.signingEndpoint`, because
|
|
2934
|
+
// provider ids (e.g. ZenMux on a mirror URL) and explicit spec overrides on
|
|
2935
|
+
// opaque proxies are authoritative even when the URL isn't recognized.
|
|
2936
|
+
// Stale-official compat never qualifies: a canonical model rerouted to an
|
|
2937
|
+
// unrecognized proxy keeps `officialEndpoint: true` (see
|
|
2938
|
+
// resolveEagerToolInputStreamingSupport), and signing there is unknowable.
|
|
2939
|
+
// Two signing routes still can't take the beta as this `anthropic-beta` HTTP
|
|
2940
|
+
// header, so they're excluded: Vertex rawPredict accepts betas only in the
|
|
2941
|
+
// JSON body (`anthropic_beta`) and 400s on the header (#5614), and GitHub
|
|
2942
|
+
// Copilot rejects Anthropic betas outright — the `github-copilot` provider
|
|
2943
|
+
// branch below strips them, but a custom provider id or a canonical model
|
|
2944
|
+
// rerouted to `api.githubcopilot.com` / `copilot-api.*` reaches the generic
|
|
2945
|
+
// header builder instead, so exclude those effective URLs here too.
|
|
2946
|
+
const needsInterleavedBeta =
|
|
2947
|
+
interleavedThinking &&
|
|
2948
|
+
(!model.thinking?.supportsDisplay ||
|
|
2949
|
+
(!isOfficialAnthropicApiUrl(baseUrl) &&
|
|
2950
|
+
(isAnthropicSigningProxyUrl(baseUrl) || (compat.signingEndpoint && !compat.officialEndpoint)) &&
|
|
2951
|
+
!isVertexRawPredictUrl(baseUrl ?? "") &&
|
|
2952
|
+
!hostMatchesUrl(baseUrl, "githubCopilot")));
|
|
2953
|
+
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
2954
|
+
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
|
2955
|
+
const needsFineGrainedToolStreamingBeta =
|
|
2956
|
+
hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
|
2957
|
+
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model, baseUrl);
|
|
2958
|
+
const tlsFetchOptions = buildCoworkTlsFetchOptions(model, baseUrl);
|
|
2959
|
+
// Disable Bun's native ~300s pre-response fetch timeout (issue #2422).
|
|
2960
|
+
// `AnthropicMessagesClient` already arms its own DEFAULT_TIMEOUT_MS timer
|
|
2961
|
+
// per request, so the native ceiling can only short-circuit slow-prefill
|
|
2962
|
+
// streams before the configured watchdog gets to govern them.
|
|
2963
|
+
const fetchOptions: AnthropicFetchOptions = { ...(tlsFetchOptions ?? {}), timeout: false };
|
|
2964
|
+
const baseFetch = args.fetch ?? fetch;
|
|
2965
|
+
// Only OAuth requests inject the CC billing header; no API-key request can ever
|
|
2966
|
+
// contain it, so there is no need to install the rewriter for those.
|
|
2967
|
+
const cchFetch = oauthToken ? wrapFetchForCch(baseFetch) : baseFetch;
|
|
2968
|
+
if (model.provider === "github-copilot") {
|
|
2969
|
+
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
2970
|
+
// The GitHub Copilot Anthropic proxy doesn't accept Anthropic beta
|
|
2971
|
+
// features. Forward only caller-supplied betas.
|
|
2972
|
+
const betaFeatures = [...extraBetas];
|
|
2973
|
+
const defaultHeaders = mergeHeaders(
|
|
2974
|
+
{
|
|
2975
|
+
Accept: stream ? "text/event-stream" : "application/json",
|
|
2976
|
+
"Content-Type": "application/json",
|
|
2977
|
+
"anthropic-version": "2023-06-01",
|
|
2978
|
+
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
2979
|
+
Authorization: `Bearer ${copilotApiKey}`,
|
|
2980
|
+
...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
|
|
2981
|
+
},
|
|
2982
|
+
model.headers,
|
|
2983
|
+
dynamicHeaders,
|
|
2984
|
+
headers,
|
|
2985
|
+
);
|
|
2986
|
+
|
|
2987
|
+
return {
|
|
2988
|
+
isOAuthToken: false,
|
|
2989
|
+
apiKey: null,
|
|
2990
|
+
authToken: copilotApiKey,
|
|
2991
|
+
baseURL: baseUrl,
|
|
2992
|
+
maxRetries: 5,
|
|
2993
|
+
maxRetryDelayMs,
|
|
2994
|
+
defaultHeaders,
|
|
2995
|
+
fetch: cchFetch,
|
|
2996
|
+
fetchOptions,
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
const betaFeatures = [...extraBetas];
|
|
3001
|
+
if (needsFineGrainedToolStreamingBeta) {
|
|
3002
|
+
betaFeatures.push(fineGrainedToolStreamingBeta);
|
|
3003
|
+
}
|
|
3004
|
+
if (needsInterleavedBeta) {
|
|
3005
|
+
betaFeatures.push(interleavedThinkingBeta);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
const defaultHeaders = buildAnthropicHeaders({
|
|
3009
|
+
apiKey,
|
|
3010
|
+
baseUrl,
|
|
3011
|
+
isOAuth: oauthToken,
|
|
3012
|
+
extraBetas: betaFeatures,
|
|
3013
|
+
stream,
|
|
3014
|
+
modelHeaders: mergeHeaders(
|
|
3015
|
+
model.headers,
|
|
3016
|
+
foundryCustomHeaders,
|
|
3017
|
+
getUmansWebSearchHeader(model, mergeHeaders(model.headers, headers)),
|
|
3018
|
+
headers,
|
|
3019
|
+
dynamicHeaders,
|
|
3020
|
+
),
|
|
3021
|
+
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
|
3022
|
+
allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides,
|
|
3023
|
+
claudeCodeSessionId,
|
|
3024
|
+
coworkBetas: oauthToken ? buildCoworkBetas(hasTools || thinkingEnabled, thinkingEnabled, disableStrictTools) : [],
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
if (model.provider === "cloudflare-ai-gateway") {
|
|
3028
|
+
return {
|
|
3029
|
+
isOAuthToken: false,
|
|
3030
|
+
apiKey: null,
|
|
3031
|
+
authToken: null,
|
|
3032
|
+
baseURL: baseUrl,
|
|
3033
|
+
maxRetries: 5,
|
|
3034
|
+
maxRetryDelayMs,
|
|
3035
|
+
defaultHeaders,
|
|
3036
|
+
fetch: cchFetch,
|
|
3037
|
+
fetchOptions,
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// OpenCode Go/Zen and Umans validate Anthropic-compatible API-key auth
|
|
3042
|
+
// through `X-Api-Key`; bearer-only requests reach the endpoint but fail auth
|
|
3043
|
+
// with `401 Missing API key` (#6510). Drop the auto-built `Authorization`
|
|
3044
|
+
// header and keep `apiKey` so the client emits `X-Api-Key`.
|
|
3045
|
+
if (model.provider === "opencode-go" || model.provider === "opencode-zen" || model.provider === "umans") {
|
|
3046
|
+
delete defaultHeaders.Authorization;
|
|
3047
|
+
return {
|
|
3048
|
+
isOAuthToken: false,
|
|
3049
|
+
apiKey,
|
|
3050
|
+
authToken: null,
|
|
3051
|
+
baseURL: baseUrl,
|
|
3052
|
+
maxRetries: 5,
|
|
3053
|
+
maxRetryDelayMs,
|
|
3054
|
+
defaultHeaders,
|
|
3055
|
+
fetch: cchFetch,
|
|
3056
|
+
fetchOptions,
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// Suppress the client-level `X-Api-Key` whenever an `Authorization` header
|
|
3061
|
+
// already sits in `defaultHeaders` for a non-official, non-OAuth endpoint —
|
|
3062
|
+
// either our auto-built `Bearer <apiKey>` or a caller-supplied custom auth
|
|
3063
|
+
// scheme via `model.headers` (#3391). Adding a bonus `X-Api-Key` would force
|
|
3064
|
+
// the proxy to deal with two competing credentials when the user explicitly
|
|
3065
|
+
// asked for one.
|
|
3066
|
+
const authorizationHeader = getHeaderCaseInsensitive(defaultHeaders, "Authorization");
|
|
3067
|
+
const shouldSuppressClientApiKey =
|
|
3068
|
+
!oauthToken && !model.compat.officialEndpoint && typeof authorizationHeader === "string";
|
|
3069
|
+
|
|
3070
|
+
return {
|
|
3071
|
+
isOAuthToken: oauthToken,
|
|
3072
|
+
apiKey: oauthToken || shouldSuppressClientApiKey ? null : apiKey,
|
|
3073
|
+
authToken: oauthToken ? apiKey : undefined,
|
|
3074
|
+
baseURL: baseUrl,
|
|
3075
|
+
maxRetries: 5,
|
|
3076
|
+
maxRetryDelayMs,
|
|
3077
|
+
defaultHeaders,
|
|
3078
|
+
fetch: cchFetch,
|
|
3079
|
+
fetchOptions,
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
function createClient(
|
|
3084
|
+
model: Model<"anthropic-messages">,
|
|
3085
|
+
args: AnthropicClientOptionsArgs,
|
|
3086
|
+
): { client: AnthropicMessagesClient; isOAuthToken: boolean } {
|
|
3087
|
+
const { isOAuthToken: oauthToken, ...clientOptions } = buildAnthropicClientOptions({ ...args, model });
|
|
3088
|
+
const client = new AnthropicMessagesClient(clientOptions);
|
|
3089
|
+
return { client, isOAuthToken: oauthToken };
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
function disableThinkingIfToolChoiceForced(
|
|
3093
|
+
params: MessageCreateParamsStreaming,
|
|
3094
|
+
model: Model<"anthropic-messages">,
|
|
3095
|
+
): void {
|
|
3096
|
+
const toolChoice = params.tool_choice;
|
|
3097
|
+
if (!toolChoice) return;
|
|
3098
|
+
if (toolChoice.type !== "any" && toolChoice.type !== "tool") return;
|
|
3099
|
+
|
|
3100
|
+
delete params.thinking;
|
|
3101
|
+
delete params.context_management;
|
|
3102
|
+
|
|
3103
|
+
// Adaptive-only models can't be switched off by omitting `thinking` — a bare
|
|
3104
|
+
// omission defaults to adaptive thinking ON, so a forced-tool turn would still
|
|
3105
|
+
// reason instead of calling the tool (#6589). Pin the lowest adaptive effort
|
|
3106
|
+
// instead of dropping it, mirroring the disable branch in buildParams. Vertex
|
|
3107
|
+
// rawPredict is the sole exception: it can only carry the effort beta in the
|
|
3108
|
+
// body (dropped there too, see buildParams), so it keeps the delete behavior.
|
|
3109
|
+
// The effort beta itself is attached at the request site — including per-request
|
|
3110
|
+
// for injected SDK clients that bypass client-level beta construction.
|
|
3111
|
+
if (isAdaptiveOnlyThinking(model) && model.provider !== "google-vertex") {
|
|
3112
|
+
const outputConfig = (params.output_config as AnthropicOutputConfig | undefined) ?? {};
|
|
3113
|
+
outputConfig.effort = "low";
|
|
3114
|
+
params.output_config = outputConfig;
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
const outputConfig = params.output_config as AnthropicOutputConfig | undefined;
|
|
3119
|
+
if (!outputConfig) return;
|
|
3120
|
+
|
|
3121
|
+
delete outputConfig.effort;
|
|
3122
|
+
if (Object.keys(outputConfig).length === 0) {
|
|
3123
|
+
delete params.output_config;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, maxAllowedTokens: number): void {
|
|
3128
|
+
const thinking = params.thinking;
|
|
3129
|
+
if (thinking?.type !== "enabled") return;
|
|
3130
|
+
|
|
3131
|
+
const budgetTokens = thinking.budget_tokens ?? 0;
|
|
3132
|
+
if (budgetTokens <= 0) return;
|
|
3133
|
+
|
|
3134
|
+
const currentMaxTokens = Math.min(params.max_tokens ?? maxAllowedTokens, maxAllowedTokens);
|
|
3135
|
+
const raisedMaxTokens = Math.min(
|
|
3136
|
+
Math.max(currentMaxTokens, budgetTokens + OUTPUT_FALLBACK_BUFFER),
|
|
3137
|
+
maxAllowedTokens,
|
|
3138
|
+
);
|
|
3139
|
+
params.max_tokens = raisedMaxTokens;
|
|
3140
|
+
|
|
3141
|
+
if (budgetTokens + OUTPUT_FALLBACK_BUFFER <= raisedMaxTokens) return;
|
|
3142
|
+
|
|
3143
|
+
const clampedBudget = raisedMaxTokens - OUTPUT_FALLBACK_BUFFER;
|
|
3144
|
+
if (clampedBudget <= 0) {
|
|
3145
|
+
throw new AIError.ConfigurationError(
|
|
3146
|
+
`Anthropic thinking budget requires max_tokens greater than ${OUTPUT_FALLBACK_BUFFER}; got ${raisedMaxTokens}`,
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
thinking.budget_tokens = clampedBudget;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
type CacheControlBlock = {
|
|
3153
|
+
cache_control?: AnthropicCacheControl | null;
|
|
3154
|
+
};
|
|
3155
|
+
|
|
3156
|
+
function applyCacheControlToLastTextBlock(
|
|
3157
|
+
blocks: Array<ContentBlockParam & CacheControlBlock>,
|
|
3158
|
+
cacheControl: AnthropicCacheControl,
|
|
3159
|
+
): boolean {
|
|
3160
|
+
if (blocks.length === 0) return false;
|
|
3161
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
3162
|
+
if (blocks[i].type === "text") {
|
|
3163
|
+
if (blocks[i].cache_control != null) return false;
|
|
3164
|
+
blocks[i] = { ...blocks[i], cache_control: cloneAnthropicCacheControl(cacheControl) };
|
|
3165
|
+
return true;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
// No text block — fall back to the last block that accepts cache_control;
|
|
3169
|
+
// thinking/redacted_thinking blocks reject the field with a 400.
|
|
3170
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
3171
|
+
const type = blocks[i].type;
|
|
3172
|
+
if (type === "thinking" || type === "redacted_thinking") continue;
|
|
3173
|
+
if (blocks[i].cache_control != null) return false;
|
|
3174
|
+
blocks[i] = { ...blocks[i], cache_control: cloneAnthropicCacheControl(cacheControl) };
|
|
3175
|
+
return true;
|
|
3176
|
+
}
|
|
3177
|
+
return false;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?: AnthropicCacheControl): void {
|
|
3181
|
+
if (!cacheControl) return;
|
|
3182
|
+
|
|
3183
|
+
const MAX_CACHE_BREAKPOINTS = 4;
|
|
3184
|
+
let cacheBreakpointsUsed = countCacheControlBreakpoints(params);
|
|
3185
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
|
|
3186
|
+
let isCCLayout = false;
|
|
3187
|
+
|
|
3188
|
+
if (params.system && Array.isArray(params.system) && params.system.length > 0) {
|
|
3189
|
+
isCCLayout = params.system[0]?.text?.startsWith(CLAUDE_BILLING_HEADER_PREFIX) === true;
|
|
3190
|
+
const maxSystemBreakpoints = Math.min(3, MAX_CACHE_BREAKPOINTS - cacheBreakpointsUsed);
|
|
3191
|
+
cacheBreakpointsUsed += cacheSystemPrefixBreakpoints(
|
|
3192
|
+
params.system as AnthropicSystemBlock[],
|
|
3193
|
+
cacheControl,
|
|
3194
|
+
maxSystemBreakpoints,
|
|
3195
|
+
isCCLayout ? firstCacheableSystemIndex(params.system as AnthropicSystemBlock[]) : 0,
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
|
|
3200
|
+
|
|
3201
|
+
// `convertAnthropicMessages` appends this neutral pad after a trailing
|
|
3202
|
+
// assistant because Anthropic rejects assistant-prefill endings. It is absent
|
|
3203
|
+
// from the next normal turn, so caching it wastes a scarce breakpoint; anchor
|
|
3204
|
+
// the cache window on the preceding real assistant instead.
|
|
3205
|
+
const trailingIndex = params.messages.length - 1;
|
|
3206
|
+
const trailingMessage = params.messages[trailingIndex];
|
|
3207
|
+
const hasTrailingAssistantPad =
|
|
3208
|
+
trailingMessage?.role === "user" &&
|
|
3209
|
+
trailingMessage.content === "Continue." &&
|
|
3210
|
+
params.messages[trailingIndex - 1]?.role === "assistant";
|
|
3211
|
+
const messageEnd = hasTrailingAssistantPad ? trailingIndex - 1 : trailingIndex;
|
|
3212
|
+
const messageWindowSize = isCCLayout ? 1 : 2;
|
|
3213
|
+
const start = Math.max(0, messageEnd - messageWindowSize + 1);
|
|
3214
|
+
for (let i = messageEnd; i >= start; i--) {
|
|
3215
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) break;
|
|
3216
|
+
const message = params.messages[i];
|
|
3217
|
+
if (!message) continue;
|
|
3218
|
+
if (typeof message.content === "string") {
|
|
3219
|
+
message.content = [
|
|
3220
|
+
{ type: "text", text: message.content, cache_control: cloneAnthropicCacheControl(cacheControl) },
|
|
3221
|
+
];
|
|
3222
|
+
cacheBreakpointsUsed++;
|
|
3223
|
+
} else if (Array.isArray(message.content) && message.content.length > 0) {
|
|
3224
|
+
if (
|
|
3225
|
+
applyCacheControlToLastTextBlock(
|
|
3226
|
+
message.content as Array<ContentBlockParam & CacheControlBlock>,
|
|
3227
|
+
cacheControl,
|
|
3228
|
+
)
|
|
3229
|
+
) {
|
|
3230
|
+
cacheBreakpointsUsed++;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
function normalizeCacheControlBlockTtl(block: CacheControlBlock, seenFiveMinute: { value: boolean }): void {
|
|
3237
|
+
const cacheControl = block.cache_control;
|
|
3238
|
+
if (!cacheControl) return;
|
|
3239
|
+
if (cacheControl.ttl !== "1h") {
|
|
3240
|
+
seenFiveMinute.value = true;
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (seenFiveMinute.value) {
|
|
3244
|
+
const normalized = cloneAnthropicCacheControl(cacheControl);
|
|
3245
|
+
delete normalized.ttl;
|
|
3246
|
+
block.cache_control = normalized;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
|
|
3251
|
+
const seenFiveMinute = { value: false };
|
|
3252
|
+
if (params.tools) {
|
|
3253
|
+
for (const tool of params.tools as Array<AnthropicWireTool & CacheControlBlock>) {
|
|
3254
|
+
normalizeCacheControlBlockTtl(tool, seenFiveMinute);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
if (params.system && Array.isArray(params.system)) {
|
|
3258
|
+
for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
|
|
3259
|
+
normalizeCacheControlBlockTtl(block, seenFiveMinute);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
for (const message of params.messages) {
|
|
3263
|
+
if (!Array.isArray(message.content)) continue;
|
|
3264
|
+
for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
|
|
3265
|
+
normalizeCacheControlBlockTtl(block, seenFiveMinute);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
function findLastCacheControlIndex<T extends CacheControlBlock>(blocks: T[]): number {
|
|
3271
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
3272
|
+
if (blocks[index]?.cache_control != null) return index;
|
|
3273
|
+
}
|
|
3274
|
+
return -1;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
function stripCacheControlExceptIndex<T extends CacheControlBlock>(
|
|
3278
|
+
blocks: T[],
|
|
3279
|
+
preserveIndex: number,
|
|
3280
|
+
excessCounter: { value: number },
|
|
3281
|
+
): void {
|
|
3282
|
+
for (let index = 0; index < blocks.length && excessCounter.value > 0; index++) {
|
|
3283
|
+
if (index === preserveIndex) continue;
|
|
3284
|
+
if (!blocks[index]?.cache_control) continue;
|
|
3285
|
+
delete blocks[index].cache_control;
|
|
3286
|
+
excessCounter.value--;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function stripAllCacheControl<T extends CacheControlBlock>(blocks: T[], excessCounter: { value: number }): void {
|
|
3291
|
+
for (const block of blocks) {
|
|
3292
|
+
if (excessCounter.value <= 0) return;
|
|
3293
|
+
if (!block.cache_control) continue;
|
|
3294
|
+
delete block.cache_control;
|
|
3295
|
+
excessCounter.value--;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function stripMessageCacheControl(
|
|
3300
|
+
messages: MessageCreateParamsStreaming["messages"],
|
|
3301
|
+
excessCounter: { value: number },
|
|
3302
|
+
): void {
|
|
3303
|
+
for (const message of messages) {
|
|
3304
|
+
if (excessCounter.value <= 0) return;
|
|
3305
|
+
if (!Array.isArray(message.content)) continue;
|
|
3306
|
+
for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
|
|
3307
|
+
if (excessCounter.value <= 0) return;
|
|
3308
|
+
if (!block.cache_control) continue;
|
|
3309
|
+
delete block.cache_control;
|
|
3310
|
+
excessCounter.value--;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
function countCacheControlBreakpoints(params: MessageCreateParamsStreaming): number {
|
|
3316
|
+
let total = 0;
|
|
3317
|
+
if (params.tools) {
|
|
3318
|
+
for (const tool of params.tools as Array<AnthropicWireTool & CacheControlBlock>) {
|
|
3319
|
+
if (tool.cache_control) total++;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
if (params.system && Array.isArray(params.system)) {
|
|
3323
|
+
for (const block of params.system as Array<AnthropicSystemBlock & CacheControlBlock>) {
|
|
3324
|
+
if (block.cache_control) total++;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
for (const message of params.messages) {
|
|
3328
|
+
if (!Array.isArray(message.content)) continue;
|
|
3329
|
+
for (const block of message.content as Array<ContentBlockParam & CacheControlBlock>) {
|
|
3330
|
+
if (block.cache_control) total++;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return total;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
|
|
3337
|
+
const total = countCacheControlBreakpoints(params);
|
|
3338
|
+
if (total <= maxBreakpoints) return;
|
|
3339
|
+
const excessCounter = { value: total - maxBreakpoints };
|
|
3340
|
+
const systemBlocks =
|
|
3341
|
+
params.system && Array.isArray(params.system)
|
|
3342
|
+
? (params.system as Array<AnthropicSystemBlock & CacheControlBlock>)
|
|
3343
|
+
: [];
|
|
3344
|
+
const toolBlocks = (params.tools ?? []) as Array<AnthropicWireTool & CacheControlBlock>;
|
|
3345
|
+
const lastSystemIndex = findLastCacheControlIndex(systemBlocks);
|
|
3346
|
+
const lastToolIndex = findLastCacheControlIndex(toolBlocks);
|
|
3347
|
+
if (systemBlocks.length > 0) {
|
|
3348
|
+
stripCacheControlExceptIndex(systemBlocks, lastSystemIndex, excessCounter);
|
|
3349
|
+
}
|
|
3350
|
+
if (excessCounter.value <= 0) return;
|
|
3351
|
+
if (toolBlocks.length > 0) {
|
|
3352
|
+
stripCacheControlExceptIndex(toolBlocks, lastToolIndex, excessCounter);
|
|
3353
|
+
}
|
|
3354
|
+
if (excessCounter.value <= 0) return;
|
|
3355
|
+
stripMessageCacheControl(params.messages, excessCounter);
|
|
3356
|
+
if (excessCounter.value <= 0) return;
|
|
3357
|
+
if (systemBlocks.length > 0) {
|
|
3358
|
+
stripAllCacheControl(systemBlocks, excessCounter);
|
|
3359
|
+
}
|
|
3360
|
+
if (excessCounter.value <= 0) return;
|
|
3361
|
+
if (toolBlocks.length > 0) {
|
|
3362
|
+
stripAllCacheControl(toolBlocks, excessCounter);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
function usesAdaptiveThinkingTagOnly(model: Model<"anthropic-messages">): boolean {
|
|
3367
|
+
const thinking = model.thinking;
|
|
3368
|
+
if (thinking?.mode !== "anthropic-adaptive") return false;
|
|
3369
|
+
const effortMap = thinking.effortMap;
|
|
3370
|
+
if (!effortMap) return false;
|
|
3371
|
+
for (const effort of thinking.efforts) {
|
|
3372
|
+
if (effortMap[effort] !== "adaptive") return false;
|
|
3373
|
+
}
|
|
3374
|
+
return thinking.efforts.length > 0;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
/**
|
|
3378
|
+
* True for adaptive-only Claude models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5)
|
|
3379
|
+
* that reject `thinking.type: "disabled"`. Turning thinking off on these models
|
|
3380
|
+
* means omitting the `thinking` field entirely and pinning the lowest adaptive
|
|
3381
|
+
* effort — a bare omission defaults to adaptive thinking ON. Excludes MiniMax,
|
|
3382
|
+
* which drives adaptive thinking through the `thinking.type: "adaptive"` tag
|
|
3383
|
+
* itself rather than `output_config.effort`.
|
|
3384
|
+
*/
|
|
3385
|
+
function isAdaptiveOnlyThinking(model: Model<"anthropic-messages">): boolean {
|
|
3386
|
+
return (
|
|
3387
|
+
model.thinking?.mode === "anthropic-adaptive" &&
|
|
3388
|
+
!model.compat.disableAdaptiveThinking &&
|
|
3389
|
+
!usesAdaptiveThinkingTagOnly(model)
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function resolveAnthropicAdaptiveEffort(
|
|
3394
|
+
model: Model<"anthropic-messages">,
|
|
3395
|
+
options: AnthropicOptions,
|
|
3396
|
+
): AnthropicEffort | undefined {
|
|
3397
|
+
if (options.effort) return usesAdaptiveThinkingTagOnly(model) ? "adaptive" : options.effort;
|
|
3398
|
+
const requestedEffort = options.reasoning;
|
|
3399
|
+
if (!requestedEffort) return undefined;
|
|
3400
|
+
return mapEffortToAnthropicAdaptiveEffort(model, requestedEffort);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): string {
|
|
3404
|
+
for (const message of messages) {
|
|
3405
|
+
if (message.role !== "user") continue;
|
|
3406
|
+
const { content } = message;
|
|
3407
|
+
if (typeof content === "string") return content;
|
|
3408
|
+
if (!Array.isArray(content)) return "";
|
|
3409
|
+
for (const block of content) {
|
|
3410
|
+
if (block.type === "text") return block.text;
|
|
3411
|
+
}
|
|
3412
|
+
return "";
|
|
3413
|
+
}
|
|
3414
|
+
return "";
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
type AnthropicParamBuildOptions = {
|
|
3418
|
+
disableStrictTools: boolean;
|
|
3419
|
+
useUmansGatewayWebSearch: boolean;
|
|
3420
|
+
forceDemoteUnsignedThinking: boolean;
|
|
3421
|
+
supportsEagerToolInputStreaming: boolean;
|
|
3422
|
+
/** Sanitized server-side fallback entries; defaults to `options?.fallbacks` when omitted. */
|
|
3423
|
+
fallbacks?: AnthropicOptions["fallbacks"];
|
|
3424
|
+
};
|
|
3425
|
+
|
|
3426
|
+
function buildParams(
|
|
3427
|
+
model: Model<"anthropic-messages">,
|
|
3428
|
+
context: Context,
|
|
3429
|
+
isOAuthToken: boolean,
|
|
3430
|
+
options: AnthropicOptions | undefined,
|
|
3431
|
+
buildOptions: AnthropicParamBuildOptions,
|
|
3432
|
+
): MessageCreateParamsStreaming {
|
|
3433
|
+
const {
|
|
3434
|
+
disableStrictTools,
|
|
3435
|
+
useUmansGatewayWebSearch,
|
|
3436
|
+
forceDemoteUnsignedThinking,
|
|
3437
|
+
supportsEagerToolInputStreaming,
|
|
3438
|
+
fallbacks = options?.fallbacks,
|
|
3439
|
+
} = buildOptions;
|
|
3440
|
+
// A session-scoped auto-demote (learned from a live signing 400) clones the
|
|
3441
|
+
// resolved compat with `replayUnsignedThinking: false` so every subsequent
|
|
3442
|
+
// downstream read (convertAnthropicMessages, transformMessages) sees the
|
|
3443
|
+
// demoted default without mutating the shared `model` reference.
|
|
3444
|
+
const effectiveModel =
|
|
3445
|
+
forceDemoteUnsignedThinking && model.compat.replayUnsignedThinking
|
|
3446
|
+
? { ...model, compat: { ...model.compat, replayUnsignedThinking: false } }
|
|
3447
|
+
: model;
|
|
3448
|
+
const { cacheControl } = getCacheControl(model, options?.cacheRetention, isOAuthToken);
|
|
3449
|
+
|
|
3450
|
+
// Pre-compute system blocks so they occupy the right slot in the serialized body.
|
|
3451
|
+
const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
|
|
3452
|
+
const firstUserMessageText = shouldInjectClaudeCodeInstruction
|
|
3453
|
+
? extractClaudeCodeFirstUserMessageText(context.messages)
|
|
3454
|
+
: "";
|
|
3455
|
+
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
3456
|
+
includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
|
|
3457
|
+
firstUserMessageText,
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
// Pre-compute tools.
|
|
3461
|
+
let tools: AnthropicWireTool[] | undefined;
|
|
3462
|
+
if (context.tools) {
|
|
3463
|
+
tools = convertTools(
|
|
3464
|
+
context.tools,
|
|
3465
|
+
isOAuthToken,
|
|
3466
|
+
disableStrictTools || model.provider === "github-copilot",
|
|
3467
|
+
supportsEagerToolInputStreaming,
|
|
3468
|
+
model.compat.escapeBuiltinToolNames,
|
|
3469
|
+
useUmansGatewayWebSearch,
|
|
3470
|
+
);
|
|
3471
|
+
} else if (isOAuthToken) {
|
|
3472
|
+
tools = [];
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// Pre-compute metadata.
|
|
3476
|
+
const metadataAccountId = readAnthropicMetadataAccountId(options?.metadata);
|
|
3477
|
+
const metadataUserId = resolveAnthropicMetadataUserId(
|
|
3478
|
+
readMetadataString(options?.metadata, "user_id") ??
|
|
3479
|
+
// Deliberately share the normalized affinity identity across Kimi's two transports.
|
|
3480
|
+
(model.provider === "kimi-code" ? getOpenAIPromptCacheKey(options) : undefined),
|
|
3481
|
+
isOAuthToken,
|
|
3482
|
+
options?.sessionId,
|
|
3483
|
+
metadataAccountId,
|
|
3484
|
+
);
|
|
3485
|
+
const metadata = metadataUserId ? { user_id: metadataUserId } : undefined;
|
|
3486
|
+
|
|
3487
|
+
// Pre-compute thinking + output_config effort.
|
|
3488
|
+
let thinking: MessageCreateParamsStreaming["thinking"] | undefined;
|
|
3489
|
+
let outputConfigEffort: AnthropicOutputEffort | undefined;
|
|
3490
|
+
if (model.reasoning) {
|
|
3491
|
+
if (options?.thinkingEnabled || model.compat.requiresThinkingEnabled) {
|
|
3492
|
+
const thinkingOptions = options ?? {};
|
|
3493
|
+
const mode = model.thinking?.mode;
|
|
3494
|
+
const effort = resolveAnthropicAdaptiveEffort(model, thinkingOptions);
|
|
3495
|
+
const compat = model.compat;
|
|
3496
|
+
if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
|
|
3497
|
+
const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
|
|
3498
|
+
// Starting with Claude Opus 4.7 and Claude Fable/Mythos 5, adaptive thinking
|
|
3499
|
+
// content is omitted from the response by default. Opt into summarized
|
|
3500
|
+
// reasoning so thinking deltas keep streaming with human-readable content for
|
|
3501
|
+
// callers that rely on it. The `display` field is gated strictly on model
|
|
3502
|
+
// support: Opus 4.6 / Sonnet 4.6+ reject it with a 400, so an explicit
|
|
3503
|
+
// `thinkingDisplay` MUST NOT force it onto a model that can't accept it.
|
|
3504
|
+
if (model.thinking?.supportsDisplay) {
|
|
3505
|
+
adaptive.display = thinkingOptions.thinkingDisplay ?? "summarized";
|
|
3506
|
+
}
|
|
3507
|
+
thinking = adaptive;
|
|
3508
|
+
if (effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
3509
|
+
} else {
|
|
3510
|
+
thinking = {
|
|
3511
|
+
type: "enabled",
|
|
3512
|
+
budget_tokens: thinkingOptions.thinkingBudgetTokens || 1024,
|
|
3513
|
+
display: thinkingOptions.thinkingDisplay ?? "summarized",
|
|
3514
|
+
};
|
|
3515
|
+
if (mode === "anthropic-budget-effort" && effort && effort !== "adaptive") outputConfigEffort = effort;
|
|
3516
|
+
}
|
|
3517
|
+
} else if (options?.thinkingEnabled === false) {
|
|
3518
|
+
if (isAdaptiveOnlyThinking(model)) {
|
|
3519
|
+
// Adaptive-only Claude models (Opus 4.6+, Sonnet 4.6+, Fable/Mythos 5) reject
|
|
3520
|
+
// `thinking.type: "disabled"` — adaptive thinking cannot be switched off.
|
|
3521
|
+
// Omit the thinking field (the API defaults to adaptive) and pin the
|
|
3522
|
+
// lowest effort so "thinking off" calls stay cheap instead of failing
|
|
3523
|
+
// the request with a 400 (a hidden-thinking toggle must never break it).
|
|
3524
|
+
// The effort field requires the `effort-2025-11-24` beta; it is attached
|
|
3525
|
+
// at the request site, including per-request for injected SDK clients.
|
|
3526
|
+
outputConfigEffort = "low";
|
|
3527
|
+
} else {
|
|
3528
|
+
thinking = { type: "disabled" };
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// Pre-compute context_management. Send keep: "all" for every enabled or
|
|
3534
|
+
// adaptive thinking request (OAuth + API-key) — not just OAuth. Without
|
|
3535
|
+
// this directive Anthropic-compatible backends (Z.AI, Kimi, DeepSeek, …)
|
|
3536
|
+
// strip the replayed thinking blocks `replayUnsignedThinking` puts back
|
|
3537
|
+
// on the wire, so the model loses the prior reasoning chain across turns
|
|
3538
|
+
// and the KV cache misses every turn (#3288). Narrowing this guard back
|
|
3539
|
+
// to `isOAuthToken` regresses every API-key thinking provider. Skip
|
|
3540
|
+
// injected clients because this code cannot add the required
|
|
3541
|
+
// `context-management-2025-06-27` beta to caller-owned SDK clients. Skip
|
|
3542
|
+
// Copilot because its proxy strips Anthropic betas and demotes thinking
|
|
3543
|
+
// blocks to text upstream, so `keep: "all"` is a no-op that risks proxy
|
|
3544
|
+
// rejection of an unrecognized field. Skip Vertex rawPredict because that
|
|
3545
|
+
// adapter requires betas in the JSON body (`anthropic_beta`) instead of the
|
|
3546
|
+
// Anthropic HTTP beta header this code can add. Skip OpenCode Zen because
|
|
3547
|
+
// its Anthropic proxy rejects the unrecognized `context_management` field
|
|
3548
|
+
// with `400 Extra inputs are not permitted` on several Claude families
|
|
3549
|
+
// (#6510) — same rationale as Copilot.
|
|
3550
|
+
const shouldKeepThinkingContext =
|
|
3551
|
+
!options?.client &&
|
|
3552
|
+
model.provider !== "github-copilot" &&
|
|
3553
|
+
model.provider !== "google-vertex" &&
|
|
3554
|
+
model.provider !== "opencode-zen" &&
|
|
3555
|
+
(thinking?.type === "adaptive" || thinking?.type === "enabled");
|
|
3556
|
+
const contextManagement = shouldKeepThinkingContext
|
|
3557
|
+
? { edits: [{ type: "clear_thinking_20251015" as const, keep: "all" as const }] }
|
|
3558
|
+
: undefined;
|
|
3559
|
+
|
|
3560
|
+
// Pre-compute output_config. Skip `effort` on Vertex rawPredict: it requires
|
|
3561
|
+
// the `effort-2025-11-24` beta, which that adapter can only accept in the body
|
|
3562
|
+
// (`anthropic_beta`), never as the `anthropic-beta` HTTP header this path sets
|
|
3563
|
+
// — so the field is dropped alongside the beta to avoid a 400 (#5614).
|
|
3564
|
+
const outputConfigEntries: AnthropicOutputConfig = {};
|
|
3565
|
+
if (outputConfigEffort && model.provider !== "google-vertex") outputConfigEntries.effort = outputConfigEffort;
|
|
3566
|
+
if (options?.taskBudget) outputConfigEntries.task_budget = options.taskBudget;
|
|
3567
|
+
const outputConfig = Object.keys(outputConfigEntries).length ? outputConfigEntries : undefined;
|
|
3568
|
+
|
|
3569
|
+
// Claude Code requests at most 64k output tokens; clamp only OAuth requests,
|
|
3570
|
+
// where the wire fingerprint must match. API-key callers keep the full model
|
|
3571
|
+
// ceiling (e.g. 128k on Opus 4.8).
|
|
3572
|
+
const modelMaxTokens = model.maxTokens ?? CLAUDE_CODE_MAX_OUTPUT_TOKENS;
|
|
3573
|
+
const maxOutputTokens = isOAuthToken ? Math.min(CLAUDE_CODE_MAX_OUTPUT_TOKENS, modelMaxTokens) : modelMaxTokens;
|
|
3574
|
+
|
|
3575
|
+
// Build params in the canonical field order: model → messages → system → tools →
|
|
3576
|
+
// metadata → max_tokens → thinking → context_management → output_config → stream.
|
|
3577
|
+
const params: MessageCreateParamsStreaming = {
|
|
3578
|
+
model: options?.requestModelId ?? model.requestModelId ?? model.id,
|
|
3579
|
+
messages: convertAnthropicMessages(context.messages, effectiveModel, isOAuthToken, {
|
|
3580
|
+
serverSideFallbackEnabled: !!fallbacks?.length,
|
|
3581
|
+
}),
|
|
3582
|
+
...(systemBlocks && { system: systemBlocks }),
|
|
3583
|
+
...(tools !== undefined && { tools }),
|
|
3584
|
+
...(metadata && { metadata }),
|
|
3585
|
+
max_tokens: Math.min(maxOutputTokens, options?.maxTokens || modelMaxTokens),
|
|
3586
|
+
...(thinking && { thinking }),
|
|
3587
|
+
...(contextManagement && { context_management: contextManagement }),
|
|
3588
|
+
...(outputConfig && { output_config: outputConfig }),
|
|
3589
|
+
...(fallbacks?.length ? { fallbacks } : {}),
|
|
3590
|
+
stream: true,
|
|
3591
|
+
};
|
|
3592
|
+
|
|
3593
|
+
// Opus 4.7+ and Fable/Mythos 5 reject non-default sampling parameters with 400 error.
|
|
3594
|
+
const thinkingType = params.thinking?.type;
|
|
3595
|
+
const allowSamplingParams =
|
|
3596
|
+
model.compat.supportsSamplingParams && (thinkingType === undefined || thinkingType === "disabled");
|
|
3597
|
+
if (allowSamplingParams && options?.temperature !== undefined) {
|
|
3598
|
+
params.temperature = options.temperature;
|
|
3599
|
+
}
|
|
3600
|
+
if (allowSamplingParams && options?.topP !== undefined) {
|
|
3601
|
+
params.top_p = options.topP;
|
|
3602
|
+
}
|
|
3603
|
+
if (allowSamplingParams && options?.topK !== undefined) {
|
|
3604
|
+
params.top_k = options.topK;
|
|
3605
|
+
}
|
|
3606
|
+
if (options?.stopSequences?.length) {
|
|
3607
|
+
const seqs = options.stopSequences;
|
|
3608
|
+
if (seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX && !warnedStopSequencesTrim) {
|
|
3609
|
+
warnedStopSequencesTrim = true;
|
|
3610
|
+
logger.warn("anthropic: stop_sequences exceeds 4; extra entries dropped", {
|
|
3611
|
+
received: seqs.length,
|
|
3612
|
+
kept: ANTHROPIC_STOP_SEQUENCES_MAX,
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
params.stop_sequences =
|
|
3616
|
+
seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
if (model.provider === "anthropic" && options?.serviceTier === "priority") {
|
|
3620
|
+
params.speed = "fast";
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
if (options?.toolChoice) {
|
|
3624
|
+
if (typeof options.toolChoice === "string") {
|
|
3625
|
+
params.tool_choice = { type: options.toolChoice };
|
|
3626
|
+
} else if (options.toolChoice.name) {
|
|
3627
|
+
params.tool_choice = {
|
|
3628
|
+
...options.toolChoice,
|
|
3629
|
+
name: encodeAnthropicToolName(
|
|
3630
|
+
options.toolChoice.name,
|
|
3631
|
+
isOAuthToken,
|
|
3632
|
+
model.compat.escapeBuiltinToolNames,
|
|
3633
|
+
useUmansGatewayWebSearch,
|
|
3634
|
+
),
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
// Claude Fable/Mythos 5 reject forced tool use outright ("tool_choice forces
|
|
3638
|
+
// tool use is not compatible with this model"). Downgrade any/tool → auto so the
|
|
3639
|
+
// request succeeds; the tool stays available and the caller's prompt steers
|
|
3640
|
+
// the model toward it.
|
|
3641
|
+
const choiceType = params.tool_choice?.type;
|
|
3642
|
+
if ((choiceType === "any" || choiceType === "tool") && !model.compat.supportsForcedToolChoice) {
|
|
3643
|
+
params.tool_choice = { type: "auto" };
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
disableThinkingIfToolChoiceForced(params, model);
|
|
3648
|
+
ensureMaxTokensForThinking(params, maxOutputTokens);
|
|
3649
|
+
applyPromptCaching(params, cacheControl);
|
|
3650
|
+
enforceCacheControlLimit(params, 4);
|
|
3651
|
+
normalizeCacheControlTtlOrdering(params);
|
|
3652
|
+
|
|
3653
|
+
return params;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
const EMPTY_ERROR_TOOL_RESULT_TEXT = "Tool failed with no output.";
|
|
3657
|
+
|
|
3658
|
+
function isEmptyToolResultWireContent(content: AnthropicToolResultContent): boolean {
|
|
3659
|
+
if (typeof content === "string") {
|
|
3660
|
+
return content.trim().length === 0;
|
|
3661
|
+
}
|
|
3662
|
+
return content.length === 0;
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
function ensureErrorToolResultWireContent(
|
|
3666
|
+
content: AnthropicToolResultContent,
|
|
3667
|
+
isError: boolean | undefined,
|
|
3668
|
+
): AnthropicToolResultContent {
|
|
3669
|
+
if (!isError || !isEmptyToolResultWireContent(content)) {
|
|
3670
|
+
return content;
|
|
3671
|
+
}
|
|
3672
|
+
return typeof content === "string"
|
|
3673
|
+
? EMPTY_ERROR_TOOL_RESULT_TEXT
|
|
3674
|
+
: [{ type: "text", text: EMPTY_ERROR_TOOL_RESULT_TEXT }];
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function buildToolResultBlock(
|
|
3678
|
+
model: Model<"anthropic-messages">,
|
|
3679
|
+
msg: ToolResultMessage,
|
|
3680
|
+
hoistedImages: ContentBlockParam[],
|
|
3681
|
+
): ContentBlockParam {
|
|
3682
|
+
let content = convertContentBlocks(msg.content, model.input.includes("image"));
|
|
3683
|
+
// Anthropic rejects images inside error tool results ("all content must be
|
|
3684
|
+
// type `text` if `is_error` is true") — keep the text in the block and
|
|
3685
|
+
// hoist the images after the message's tool_result run.
|
|
3686
|
+
if (msg.isError && typeof content !== "string" && content.some(block => block.type === "image")) {
|
|
3687
|
+
for (const block of content) {
|
|
3688
|
+
if (block.type === "image") hoistedImages.push(block);
|
|
3689
|
+
}
|
|
3690
|
+
content = content.filter(block => block.type === "text");
|
|
3691
|
+
}
|
|
3692
|
+
content = ensureErrorToolResultWireContent(content, msg.isError);
|
|
3693
|
+
const block: ContentBlockParam = {
|
|
3694
|
+
type: "tool_result",
|
|
3695
|
+
tool_use_id: msg.toolCallId,
|
|
3696
|
+
content,
|
|
3697
|
+
is_error: msg.isError,
|
|
3698
|
+
};
|
|
3699
|
+
if (model.compat.requiresToolResultId) {
|
|
3700
|
+
// Z.AI workaround (issue #814): include `id` aliased to `tool_use_id`.
|
|
3701
|
+
(block as unknown as Record<string, unknown>).id = msg.toolCallId;
|
|
3702
|
+
}
|
|
3703
|
+
return block;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* A single Anthropic conversation turn, including the mid-conversation
|
|
3708
|
+
* `system` role (Opus 4.8+ and Fable/Mythos 5).
|
|
3709
|
+
*/
|
|
3710
|
+
export type AnthropicMessageParam = MessageParam;
|
|
3711
|
+
|
|
3712
|
+
/**
|
|
3713
|
+
* Recursively replace lone surrogates in string leaves. Identity-preserving:
|
|
3714
|
+
* returns the input object/array when nothing changed.
|
|
3715
|
+
*/
|
|
3716
|
+
function toWellFormedDeep(value: unknown): unknown {
|
|
3717
|
+
if (typeof value === "string") {
|
|
3718
|
+
const wellFormed = value.toWellFormed();
|
|
3719
|
+
return wellFormed === value ? value : wellFormed;
|
|
3720
|
+
}
|
|
3721
|
+
if (Array.isArray(value)) {
|
|
3722
|
+
let changed = false;
|
|
3723
|
+
const next = value.map(entry => {
|
|
3724
|
+
const sanitized = toWellFormedDeep(entry);
|
|
3725
|
+
if (sanitized !== entry) changed = true;
|
|
3726
|
+
return sanitized;
|
|
3727
|
+
});
|
|
3728
|
+
return changed ? next : value;
|
|
3729
|
+
}
|
|
3730
|
+
if (isRecord(value)) {
|
|
3731
|
+
let changed = false;
|
|
3732
|
+
const next: Record<string, unknown> = {};
|
|
3733
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3734
|
+
const sanitized = toWellFormedDeep(entry);
|
|
3735
|
+
if (sanitized !== entry) changed = true;
|
|
3736
|
+
next[key] = sanitized;
|
|
3737
|
+
}
|
|
3738
|
+
return changed ? next : value;
|
|
3739
|
+
}
|
|
3740
|
+
return value;
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
/**
|
|
3744
|
+
* Serialize omp {@link Message}s to Anthropic wire messages.
|
|
3745
|
+
*
|
|
3746
|
+
* `opts.serverSideFallbackEnabled` — when the CURRENT request itself
|
|
3747
|
+
* opts into the server-side-fallback beta chain. Only then may a persisted
|
|
3748
|
+
* `fallback` content block from a prior turn be replayed on the wire;
|
|
3749
|
+
* otherwise the block is dropped to avoid a 400 on non-fallback requests
|
|
3750
|
+
* that don't send the beta.
|
|
3751
|
+
*/
|
|
3752
|
+
export function convertAnthropicMessages(
|
|
3753
|
+
messages: Message[],
|
|
3754
|
+
model: Model<"anthropic-messages">,
|
|
3755
|
+
isOAuthToken: boolean,
|
|
3756
|
+
opts?: { serverSideFallbackEnabled?: boolean },
|
|
3757
|
+
): AnthropicMessageParam[] {
|
|
3758
|
+
// Indices of params emitted from `developer` messages. After the main pass,
|
|
3759
|
+
// the ones whose placement satisfies Anthropic's mid-conversation rules are
|
|
3760
|
+
// upgraded from the `user` role to the authoritative `system` role.
|
|
3761
|
+
const developerParamIndices: number[] = [];
|
|
3762
|
+
const params: AnthropicMessageParam[] = [];
|
|
3763
|
+
|
|
3764
|
+
const transformedMessages = transformMessages(messages, model, normalizeToolCallId);
|
|
3765
|
+
|
|
3766
|
+
for (let i = 0; i < transformedMessages.length; i++) {
|
|
3767
|
+
const msg = transformedMessages[i];
|
|
3768
|
+
|
|
3769
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
3770
|
+
if (!msg.content) continue;
|
|
3771
|
+
|
|
3772
|
+
let content: string | ContentBlockParam[];
|
|
3773
|
+
if (typeof msg.content === "string") {
|
|
3774
|
+
if (msg.content.trim().length === 0) continue;
|
|
3775
|
+
content = msg.content.toWellFormed();
|
|
3776
|
+
} else {
|
|
3777
|
+
const contentBlocks = convertContentBlocks(msg.content, model.input.includes("image"));
|
|
3778
|
+
if (typeof contentBlocks === "string") {
|
|
3779
|
+
if (contentBlocks.trim().length === 0) continue;
|
|
3780
|
+
content = contentBlocks;
|
|
3781
|
+
} else {
|
|
3782
|
+
if (contentBlocks.length === 0) continue;
|
|
3783
|
+
content = contentBlocks;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
if (msg.role === "developer") developerParamIndices.push(params.length);
|
|
3787
|
+
params.push({ role: "user", content });
|
|
3788
|
+
} else if (msg.role === "assistant") {
|
|
3789
|
+
const blocks: ContentBlockParam[] = [];
|
|
3790
|
+
const hasSignedThinking = msg.content.some(
|
|
3791
|
+
block =>
|
|
3792
|
+
block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
|
|
3793
|
+
);
|
|
3794
|
+
|
|
3795
|
+
for (const block of msg.content) {
|
|
3796
|
+
if (block.type === "text") {
|
|
3797
|
+
if (block.text.trim().length === 0) continue;
|
|
3798
|
+
blocks.push({
|
|
3799
|
+
type: "text",
|
|
3800
|
+
text: block.text.toWellFormed(),
|
|
3801
|
+
});
|
|
3802
|
+
} else if (block.type === "thinking") {
|
|
3803
|
+
if (hasSignedThinking) {
|
|
3804
|
+
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
3805
|
+
if (block.thinking.trim().length === 0) continue;
|
|
3806
|
+
blocks.push({
|
|
3807
|
+
type: "text",
|
|
3808
|
+
text: renderDemotedThinking(model.id, block.thinking),
|
|
3809
|
+
});
|
|
3810
|
+
continue;
|
|
3811
|
+
}
|
|
3812
|
+
blocks.push({
|
|
3813
|
+
type: "thinking",
|
|
3814
|
+
thinking: block.thinking,
|
|
3815
|
+
signature: block.thinkingSignature,
|
|
3816
|
+
});
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
if (block.thinking.trim().length === 0) continue;
|
|
3820
|
+
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
3821
|
+
if (model.compat.replayUnsignedThinking) {
|
|
3822
|
+
blocks.push({
|
|
3823
|
+
type: "thinking",
|
|
3824
|
+
thinking: block.thinking.toWellFormed(),
|
|
3825
|
+
signature: "",
|
|
3826
|
+
});
|
|
3827
|
+
} else {
|
|
3828
|
+
blocks.push({
|
|
3829
|
+
type: "text",
|
|
3830
|
+
text: renderDemotedThinking(model.id, block.thinking),
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
} else {
|
|
3834
|
+
blocks.push({
|
|
3835
|
+
type: "thinking",
|
|
3836
|
+
thinking: block.thinking.toWellFormed(),
|
|
3837
|
+
signature: block.thinkingSignature,
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
} else if (block.type === "redactedThinking") {
|
|
3841
|
+
if (block.data.trim().length === 0) continue;
|
|
3842
|
+
blocks.push({
|
|
3843
|
+
type: "redacted_thinking",
|
|
3844
|
+
data: block.data,
|
|
3845
|
+
});
|
|
3846
|
+
} else if (block.type === "anthropicServerTool") {
|
|
3847
|
+
blocks.push(block.block);
|
|
3848
|
+
} else if (block.type === "fallback") {
|
|
3849
|
+
// Replay ONLY when both sides are aligned: the current
|
|
3850
|
+
// request opted into the beta chain, and the target is
|
|
3851
|
+
// official Anthropic (the only endpoint that accepts the
|
|
3852
|
+
// block on the wire). `transformMessages` already drops
|
|
3853
|
+
// the block for cross-provider / non-official replays, so
|
|
3854
|
+
// this is defense-in-depth for direct convert calls.
|
|
3855
|
+
if (!opts?.serverSideFallbackEnabled || !model.compat.officialEndpoint) continue;
|
|
3856
|
+
blocks.push({
|
|
3857
|
+
type: "fallback",
|
|
3858
|
+
from: block.from,
|
|
3859
|
+
to: block.to,
|
|
3860
|
+
});
|
|
3861
|
+
} else if (block.type === "toolCall") {
|
|
3862
|
+
blocks.push({
|
|
3863
|
+
type: "tool_use",
|
|
3864
|
+
id: block.id,
|
|
3865
|
+
name: encodeAnthropicToolName(block.name, isOAuthToken, model.compat.escapeBuiltinToolNames),
|
|
3866
|
+
// Always sanitize: the model itself can emit lone-surrogate escapes
|
|
3867
|
+
// in tool-argument JSON (streamed out fine, rejected with a 400 on
|
|
3868
|
+
// replay by Anthropic's strict UTF-8 validation). toWellFormedDeep
|
|
3869
|
+
// is identity-preserving, so well-formed arguments stay
|
|
3870
|
+
// byte-identical and prompt-cache prefixes are unaffected.
|
|
3871
|
+
input: toWellFormedDeep(block.arguments ?? {}),
|
|
3872
|
+
});
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
// Anthropic's replay validator rejects any non-`tool_use` block that
|
|
3876
|
+
// appears after a `tool_use` inside an assistant turn (400:
|
|
3877
|
+
// "tool_use ids were found without tool_result blocks immediately
|
|
3878
|
+
// after: <id>"). A persisted turn can violate this when a mid-turn
|
|
3879
|
+
// server-side fallback handoff lands after the primary model already
|
|
3880
|
+
// emitted a tool_use — the replayed content is then e.g.
|
|
3881
|
+
// [thinking, text, tool_use, fallback, text, tool_use] — and also for
|
|
3882
|
+
// the older cross-provider [text, tool_use, text] shape (issue #544).
|
|
3883
|
+
// Stable-partition into [...non-tool_use, ...tool_use], preserving each
|
|
3884
|
+
// side's relative order: the non-tool_use chain (thinking → text →
|
|
3885
|
+
// fallback → text) carries thinking signatures and the fallback
|
|
3886
|
+
// boundary marker whose order Anthropic verifies, while tool_use blocks
|
|
3887
|
+
// are unsigned and safe to defer to the tail. Fast-path untouched when
|
|
3888
|
+
// already in order so prompt-cache prefixes stay byte-identical.
|
|
3889
|
+
let sawToolUse = false;
|
|
3890
|
+
let needsPartition = false;
|
|
3891
|
+
for (const block of blocks) {
|
|
3892
|
+
if (block.type === "tool_use") {
|
|
3893
|
+
sawToolUse = true;
|
|
3894
|
+
} else if (sawToolUse) {
|
|
3895
|
+
needsPartition = true;
|
|
3896
|
+
break;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
if (needsPartition) {
|
|
3900
|
+
const nonToolUse: ContentBlockParam[] = [];
|
|
3901
|
+
const toolUse: ContentBlockParam[] = [];
|
|
3902
|
+
for (const block of blocks) {
|
|
3903
|
+
if (block.type === "tool_use") toolUse.push(block);
|
|
3904
|
+
else nonToolUse.push(block);
|
|
3905
|
+
}
|
|
3906
|
+
blocks.length = 0;
|
|
3907
|
+
blocks.push(...nonToolUse, ...toolUse);
|
|
3908
|
+
}
|
|
3909
|
+
if (blocks.length === 0) continue;
|
|
3910
|
+
params.push({
|
|
3911
|
+
role: "assistant",
|
|
3912
|
+
content: blocks,
|
|
3913
|
+
});
|
|
3914
|
+
} else if (msg.role === "toolResult") {
|
|
3915
|
+
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
|
|
3916
|
+
const toolResults: ContentBlockParam[] = [];
|
|
3917
|
+
// Images stripped out of error tool results, re-attached after the run.
|
|
3918
|
+
const hoistedImages: ContentBlockParam[] = [];
|
|
3919
|
+
|
|
3920
|
+
// Add the current tool result
|
|
3921
|
+
toolResults.push(buildToolResultBlock(model, msg, hoistedImages));
|
|
3922
|
+
|
|
3923
|
+
// Look ahead for consecutive toolResult messages
|
|
3924
|
+
let j = i + 1;
|
|
3925
|
+
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
|
3926
|
+
const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
|
|
3927
|
+
toolResults.push(buildToolResultBlock(model, nextMsg, hoistedImages));
|
|
3928
|
+
j++;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
// Skip the messages we've already processed
|
|
3932
|
+
i = j - 1;
|
|
3933
|
+
|
|
3934
|
+
if (hoistedImages.length > 0) {
|
|
3935
|
+
toolResults.push(
|
|
3936
|
+
{ type: "text", text: "Attached image(s) from the tool result(s) above:" },
|
|
3937
|
+
...hoistedImages,
|
|
3938
|
+
);
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
// Add a single user message with all tool results
|
|
3942
|
+
params.push({
|
|
3943
|
+
role: "user",
|
|
3944
|
+
content: toolResults,
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
// Upgrade developer-origin params to mid-conversation `system` messages where
|
|
3950
|
+
// Anthropic's placement rules allow it (Opus 4.8+ / Fable/Mythos 5 on first-party API).
|
|
3951
|
+
// Rules: a system message must immediately follow a `user` turn and must be
|
|
3952
|
+
// the last entry or be followed by an `assistant` turn — never first, and
|
|
3953
|
+
// never consecutive. Requiring the next param to be `assistant` (or absent)
|
|
3954
|
+
// covers both the "followed by assistant / last" and "no consecutive system"
|
|
3955
|
+
// constraints. Anything that does not qualify stays a `user` message.
|
|
3956
|
+
if (developerParamIndices.length > 0 && model.compat.supportsMidConversationSystem) {
|
|
3957
|
+
for (const idx of developerParamIndices) {
|
|
3958
|
+
const followsUser = idx > 0 && params[idx - 1]?.role === "user";
|
|
3959
|
+
const next = params[idx + 1];
|
|
3960
|
+
const lastOrBeforeAssistant = idx === params.length - 1 || next?.role === "assistant";
|
|
3961
|
+
// System content is text-only on the wire; a developer turn carrying
|
|
3962
|
+
// image blocks must stay a `user` message or the API rejects it.
|
|
3963
|
+
const content = params[idx].content;
|
|
3964
|
+
const textOnly = typeof content === "string" || content.every(block => block.type === "text");
|
|
3965
|
+
if (followsUser && lastOrBeforeAssistant && textOnly) {
|
|
3966
|
+
params[idx] = { role: "system", content };
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
// Dropped empty user/developer turns can leave two assistant params adjacent;
|
|
3971
|
+
// the API rejects consecutive assistant messages. Repair with the same neutral
|
|
3972
|
+
// nudge used for trailing-assistant prefill below.
|
|
3973
|
+
for (let i = params.length - 1; i > 0; i--) {
|
|
3974
|
+
if (params[i].role === "assistant" && params[i - 1]?.role === "assistant") {
|
|
3975
|
+
params.splice(i, 0, { role: "user", content: "Continue." });
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
|
|
3979
|
+
params.push({ role: "user", content: "Continue." });
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
return params;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
/**
|
|
3986
|
+
* JSON Schema whitelist for Anthropic tool `input_schema` nodes.
|
|
3987
|
+
*
|
|
3988
|
+
* Tracks the Anthropic Python SDK's `lib/_parse/_transform.py::transform_schema`,
|
|
3989
|
+
* with live Messages API guardrails for keywords the SDK preserves but the API rejects.
|
|
3990
|
+
* We keep only structural/metadata keywords Anthropic's validator honors, and demote
|
|
3991
|
+
* anything else into the node's `description` as `\n\n{key: value, ...}` so the model
|
|
3992
|
+
* still sees the constraint as a natural-language hint.
|
|
3993
|
+
*
|
|
3994
|
+
* `Set` (not `Record<string, true>`) because membership is probed against arbitrary
|
|
3995
|
+
* user/Zod-derived schema keys: a literal Record would falsely match prototype names
|
|
3996
|
+
* like `"toString"` and silently strip valid properties.
|
|
3997
|
+
*/
|
|
3998
|
+
const ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP = new Set([
|
|
3999
|
+
"$ref",
|
|
4000
|
+
"$defs",
|
|
4001
|
+
"$schema",
|
|
4002
|
+
"definitions",
|
|
4003
|
+
"type",
|
|
4004
|
+
"anyOf",
|
|
4005
|
+
"allOf",
|
|
4006
|
+
"enum",
|
|
4007
|
+
"const",
|
|
4008
|
+
"description",
|
|
4009
|
+
"title",
|
|
4010
|
+
"default",
|
|
4011
|
+
"nullable",
|
|
4012
|
+
]);
|
|
4013
|
+
/** Keys preserved on `type: "object"` nodes (in addition to the universal set). */
|
|
4014
|
+
const ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP = new Set(["properties", "required", "additionalProperties"]);
|
|
4015
|
+
/** Keys preserved on `type: "array"` nodes; `minItems` only when its value is 0 or 1. */
|
|
4016
|
+
const ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP = new Set(["items", "prefixItems", "minItems"]);
|
|
4017
|
+
/** Keys preserved on `type: "string"` nodes; `format` only when its value is in the supported list. */
|
|
4018
|
+
const ANTHROPIC_TOOL_SCHEMA_STRING_KEEP = new Set(["format"]);
|
|
4019
|
+
/**
|
|
4020
|
+
* String `format` values Anthropic accepts; everything else (including `pattern`-style
|
|
4021
|
+
* format hints) gets demoted into `description`. Matches `SupportedStringFormats` in the
|
|
4022
|
+
* Anthropic SDK's `_transform.py`.
|
|
4023
|
+
*/
|
|
4024
|
+
const ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS = new Set([
|
|
4025
|
+
"date-time",
|
|
4026
|
+
"time",
|
|
4027
|
+
"date",
|
|
4028
|
+
"duration",
|
|
4029
|
+
"email",
|
|
4030
|
+
"hostname",
|
|
4031
|
+
"uri",
|
|
4032
|
+
"ipv4",
|
|
4033
|
+
"ipv6",
|
|
4034
|
+
"uuid",
|
|
4035
|
+
]);
|
|
4036
|
+
const ANTHROPIC_STRICT_TOOL_ALLOWLIST = new Set(["bash", "python", "edit", "find"]);
|
|
4037
|
+
const MAX_ANTHROPIC_STRICT_TOOLS = 20;
|
|
4038
|
+
const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
|
|
4039
|
+
const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
|
|
4040
|
+
|
|
4041
|
+
/** `minItems` / `maxItems` apply to arrays; Anthropic rejects them on `type: "object"` (including `minItems: 0`/`1`). */
|
|
4042
|
+
function isJsonSchemaArrayNode(schema: Record<string, unknown>): boolean {
|
|
4043
|
+
const t = schema.type;
|
|
4044
|
+
if (t === "array") return true;
|
|
4045
|
+
if (Array.isArray(t) && t.includes("array") && !t.includes("object")) return true;
|
|
4046
|
+
if (schema.items !== undefined || Array.isArray(schema.prefixItems)) return true;
|
|
4047
|
+
return false;
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
function isJsonSchemaObjectNode(schema: Record<string, unknown>): boolean {
|
|
4051
|
+
if (isJsonSchemaArrayNode(schema)) return false;
|
|
4052
|
+
if (schema.type === "object") return true;
|
|
4053
|
+
if (Array.isArray(schema.type) && schema.type.includes("object")) return true;
|
|
4054
|
+
if (isRecord(schema.properties)) return true;
|
|
4055
|
+
return false;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
/**
|
|
4059
|
+
* Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
|
|
4060
|
+
* `type` as either a single string or an array (e.g. `["number", "null"]` for a
|
|
4061
|
+
* nullable value); the SDK whitelist is keyed off the scalar type, with `"null"`
|
|
4062
|
+
* ignored so nullable variants are normalized as their underlying type.
|
|
4063
|
+
*/
|
|
4064
|
+
function pickAnthropicScalarType(type: unknown): string | undefined {
|
|
4065
|
+
if (typeof type === "string") return type;
|
|
4066
|
+
if (Array.isArray(type)) {
|
|
4067
|
+
for (const entry of type) {
|
|
4068
|
+
if (typeof entry === "string" && entry !== "null") return entry;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
return undefined;
|
|
4072
|
+
}
|
|
4073
|
+
function pickAnthropicEffectiveScalarType(schema: Record<string, unknown>): string | undefined {
|
|
4074
|
+
const explicit = pickAnthropicScalarType(schema.type);
|
|
4075
|
+
if (explicit) return explicit;
|
|
4076
|
+
if (isRecord(schema.properties)) return "object";
|
|
4077
|
+
if (schema.items !== undefined || Array.isArray(schema.prefixItems)) return "array";
|
|
4078
|
+
return undefined;
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
function anthropicPerTypeKeep(scalarType: string | undefined): Set<string> | undefined {
|
|
4082
|
+
switch (scalarType) {
|
|
4083
|
+
case "object":
|
|
4084
|
+
return ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP;
|
|
4085
|
+
case "array":
|
|
4086
|
+
return ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP;
|
|
4087
|
+
case "string":
|
|
4088
|
+
return ANTHROPIC_TOOL_SCHEMA_STRING_KEEP;
|
|
4089
|
+
default:
|
|
4090
|
+
return undefined;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
/**
|
|
4095
|
+
* Normalize a JSON Schema node for Anthropic tool `input_schema`.
|
|
4096
|
+
*
|
|
4097
|
+
* Applies the full whitelist semantics from the Anthropic Python SDK's
|
|
4098
|
+
* `lib/_parse/_transform.py::transform_schema`:
|
|
4099
|
+
*
|
|
4100
|
+
* 1. Universal keys (`$ref`, `$defs`, `type`, `anyOf`, `allOf`, `enum`, `const`,
|
|
4101
|
+
* `description`, `title`, `default`, `nullable`) are preserved on every node, with
|
|
4102
|
+
* one position-dependent exception: the combinator keys. Root `anyOf`/`allOf` are
|
|
4103
|
+
* spilled (recent Anthropic Messages validators reject combinators at the tool
|
|
4104
|
+
* `input_schema` root) but kept when nested; `oneOf` is spilled at every position
|
|
4105
|
+
* (it is not in the documented supported subset).
|
|
4106
|
+
* 2. Per-type keys are kept additively (object → `properties`/`required`/`additionalProperties`,
|
|
4107
|
+
* array → `items`/`prefixItems` plus `minItems` only when 0 or 1, string → `format`
|
|
4108
|
+
* only when in the supported value set).
|
|
4109
|
+
* 3. Everything else is demoted into the node's `description` as `\n\n{key: value, ...}`
|
|
4110
|
+
*
|
|
4111
|
+
* Object nodes default to `additionalProperties: false`, but explicit open-map
|
|
4112
|
+
* declarations (`additionalProperties: true` or a schema literal — Zod's
|
|
4113
|
+
* `z.record(z.string(), z.unknown())` produces `{}`) are preserved. The strict-mode
|
|
4114
|
+
* pass downstream demotes those shapes to non-strict instead of fabricating a closed
|
|
4115
|
+
* object, so callers like the resolve tool keep working open-map semantics.
|
|
4116
|
+
*/
|
|
4117
|
+
function normalizeAnthropicToolSchemaNode(
|
|
4118
|
+
schema: unknown,
|
|
4119
|
+
cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
|
|
4120
|
+
isRoot = false,
|
|
4121
|
+
): unknown {
|
|
4122
|
+
if (Array.isArray(schema)) return schema.map(entry => normalizeAnthropicToolSchemaNode(entry, cache));
|
|
4123
|
+
if (!isRecord(schema)) return schema;
|
|
4124
|
+
|
|
4125
|
+
const existing = cache.get(schema);
|
|
4126
|
+
if (existing !== undefined) return existing;
|
|
4127
|
+
|
|
4128
|
+
const result: Record<string, unknown> = {};
|
|
4129
|
+
cache.set(schema, result);
|
|
4130
|
+
|
|
4131
|
+
const scalarType = pickAnthropicEffectiveScalarType(schema);
|
|
4132
|
+
const perTypeKeep = anthropicPerTypeKeep(scalarType);
|
|
4133
|
+
const spill: Array<[string, unknown]> = [];
|
|
4134
|
+
|
|
4135
|
+
for (const key in schema) {
|
|
4136
|
+
if (!Object.hasOwn(schema, key)) continue;
|
|
4137
|
+
const value = schema[key];
|
|
4138
|
+
const isRootCombinator = isRoot && COMBINATOR_KEYS.includes(key as (typeof COMBINATOR_KEYS)[number]);
|
|
4139
|
+
if (!isRootCombinator && (ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP.has(key) || perTypeKeep?.has(key))) {
|
|
4140
|
+
result[key] = value;
|
|
4141
|
+
} else {
|
|
4142
|
+
spill.push([key, value]);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
// Per-type conditional keys: prune within the kept set.
|
|
4147
|
+
if (scalarType === "string") {
|
|
4148
|
+
const format = result.format;
|
|
4149
|
+
if (typeof format === "string" && !ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS.has(format)) {
|
|
4150
|
+
spill.push(["format", format]);
|
|
4151
|
+
delete result.format;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
if (scalarType === "array" && result.minItems !== undefined) {
|
|
4155
|
+
const minItems = result.minItems;
|
|
4156
|
+
if (!(typeof minItems === "number" && (minItems === 0 || minItems === 1))) {
|
|
4157
|
+
spill.push(["minItems", minItems]);
|
|
4158
|
+
delete result.minItems;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
if (scalarType === "object" && result.additionalProperties === undefined) {
|
|
4162
|
+
result.additionalProperties = false;
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
// Recurse on structural keys.
|
|
4166
|
+
if (isRecord(result.properties)) {
|
|
4167
|
+
const normalizedProperties: Record<string, unknown> = {};
|
|
4168
|
+
const sourceProperties = result.properties as Record<string, unknown>;
|
|
4169
|
+
for (const propName in sourceProperties) {
|
|
4170
|
+
if (!Object.hasOwn(sourceProperties, propName)) continue;
|
|
4171
|
+
normalizedProperties[propName] = normalizeAnthropicToolSchemaNode(sourceProperties[propName], cache);
|
|
4172
|
+
}
|
|
4173
|
+
result.properties = normalizedProperties;
|
|
4174
|
+
}
|
|
4175
|
+
if (isRecord(result.additionalProperties)) {
|
|
4176
|
+
const normalized = normalizeAnthropicToolSchemaNode(result.additionalProperties, cache);
|
|
4177
|
+
if (isRecord(normalized) && Object.keys(normalized).length === 0) {
|
|
4178
|
+
result.additionalProperties = true;
|
|
4179
|
+
} else {
|
|
4180
|
+
result.additionalProperties = normalized;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
if (Array.isArray(result.items)) {
|
|
4184
|
+
result.items = result.items.map(item => normalizeAnthropicToolSchemaNode(item, cache));
|
|
4185
|
+
} else if (isRecord(result.items)) {
|
|
4186
|
+
result.items = normalizeAnthropicToolSchemaNode(result.items, cache);
|
|
4187
|
+
}
|
|
4188
|
+
if (Array.isArray(result.prefixItems)) {
|
|
4189
|
+
result.prefixItems = result.prefixItems.map(item => normalizeAnthropicToolSchemaNode(item, cache));
|
|
4190
|
+
}
|
|
4191
|
+
for (const key of COMBINATOR_KEYS) {
|
|
4192
|
+
const variants = result[key];
|
|
4193
|
+
if (Array.isArray(variants)) {
|
|
4194
|
+
result[key] = variants.map(variant => normalizeAnthropicToolSchemaNode(variant, cache));
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
for (const defsKey of ["$defs", "definitions"] as const) {
|
|
4198
|
+
const definitions = result[defsKey];
|
|
4199
|
+
if (!isRecord(definitions)) continue;
|
|
4200
|
+
const normalizedDefs: Record<string, unknown> = {};
|
|
4201
|
+
const sourceDefs = definitions as Record<string, unknown>;
|
|
4202
|
+
for (const name in sourceDefs) {
|
|
4203
|
+
if (!Object.hasOwn(sourceDefs, name)) continue;
|
|
4204
|
+
normalizedDefs[name] = normalizeAnthropicToolSchemaNode(sourceDefs[name], cache);
|
|
4205
|
+
}
|
|
4206
|
+
result[defsKey] = normalizedDefs;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
spillToDescription(result, spill);
|
|
4210
|
+
return result;
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
export function normalizeAnthropicToolSchema(schema: unknown): unknown {
|
|
4214
|
+
return normalizeAnthropicToolSchemaNode(schema, new WeakMap(), true);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
type AnthropicToolSchemaPlan = {
|
|
4218
|
+
inputSchema: AnthropicToolInputSchema;
|
|
4219
|
+
strict: boolean;
|
|
4220
|
+
};
|
|
4221
|
+
|
|
4222
|
+
type AnthropicStrictBudget = {
|
|
4223
|
+
optionalRemaining: number;
|
|
4224
|
+
unionRemaining: number;
|
|
4225
|
+
optionalCount: number;
|
|
4226
|
+
unionCount: number;
|
|
4227
|
+
};
|
|
4228
|
+
|
|
4229
|
+
function hasAnthropicUnionType(schema: Record<string, unknown>): boolean {
|
|
4230
|
+
return Array.isArray(schema.type) || Array.isArray(schema.anyOf);
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
function hasNullVariant(schema: Record<string, unknown>): boolean {
|
|
4234
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
4235
|
+
return Array.isArray(schema.anyOf) && schema.anyOf.some(variant => isRecord(variant) && variant.type === "null");
|
|
4236
|
+
}
|
|
4237
|
+
function hasAnthropicSchemaDefiningKeyword(schema: Record<string, unknown>): boolean {
|
|
4238
|
+
if (
|
|
4239
|
+
schema.type !== undefined ||
|
|
4240
|
+
schema.properties !== undefined ||
|
|
4241
|
+
schema.additionalProperties !== undefined ||
|
|
4242
|
+
schema.items !== undefined ||
|
|
4243
|
+
schema.prefixItems !== undefined ||
|
|
4244
|
+
schema.enum !== undefined ||
|
|
4245
|
+
schema.const !== undefined ||
|
|
4246
|
+
schema.$ref !== undefined
|
|
4247
|
+
) {
|
|
4248
|
+
return true;
|
|
4249
|
+
}
|
|
4250
|
+
for (const key of COMBINATOR_KEYS) {
|
|
4251
|
+
if (schema[key] !== undefined) return true;
|
|
4252
|
+
}
|
|
4253
|
+
return schema.$defs !== undefined || schema.definitions !== undefined;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function makeAnthropicNullableSchema(schema: unknown, budget: AnthropicStrictBudget): unknown | undefined {
|
|
4257
|
+
if (isRecord(schema)) {
|
|
4258
|
+
if (hasNullVariant(schema)) return schema;
|
|
4259
|
+
if (Array.isArray(schema.anyOf)) {
|
|
4260
|
+
return { ...schema, anyOf: [...schema.anyOf, { type: "null" }] };
|
|
4261
|
+
}
|
|
4262
|
+
if (Array.isArray(schema.type)) {
|
|
4263
|
+
return { ...schema, type: [...schema.type, "null"] };
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
if (budget.unionRemaining <= 0) return undefined;
|
|
4268
|
+
budget.unionRemaining--;
|
|
4269
|
+
budget.unionCount++;
|
|
4270
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
function normalizeAnthropicStrictSchemaNode(
|
|
4274
|
+
schema: unknown,
|
|
4275
|
+
budget: AnthropicStrictBudget,
|
|
4276
|
+
cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
|
|
4277
|
+
): unknown | undefined {
|
|
4278
|
+
if (Array.isArray(schema)) {
|
|
4279
|
+
const result: unknown[] = [];
|
|
4280
|
+
for (const entry of schema) {
|
|
4281
|
+
const normalized = normalizeAnthropicStrictSchemaNode(entry, budget, cache);
|
|
4282
|
+
if (normalized === undefined) return undefined;
|
|
4283
|
+
result.push(normalized);
|
|
4284
|
+
}
|
|
4285
|
+
return result;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
if (!isRecord(schema)) return schema;
|
|
4289
|
+
|
|
4290
|
+
const cached = cache.get(schema);
|
|
4291
|
+
if (cached) return cached;
|
|
4292
|
+
|
|
4293
|
+
if (!hasAnthropicSchemaDefiningKeyword(schema)) return undefined;
|
|
4294
|
+
|
|
4295
|
+
// Strict tool use only supports closed objects. Open maps stay available on
|
|
4296
|
+
// the non-strict schema plan instead of producing an Anthropic 400.
|
|
4297
|
+
if (isJsonSchemaObjectNode(schema) && schema.additionalProperties !== false) {
|
|
4298
|
+
return undefined;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
const result: Record<string, unknown> = { ...schema };
|
|
4302
|
+
cache.set(schema, result);
|
|
4303
|
+
|
|
4304
|
+
if (hasAnthropicUnionType(result)) {
|
|
4305
|
+
if (budget.unionRemaining <= 0) return undefined;
|
|
4306
|
+
budget.unionRemaining--;
|
|
4307
|
+
budget.unionCount++;
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
if (isRecord(result.properties)) {
|
|
4311
|
+
const originalRequired = new Set(
|
|
4312
|
+
Array.isArray(result.required)
|
|
4313
|
+
? result.required.filter((entry): entry is string => typeof entry === "string")
|
|
4314
|
+
: [],
|
|
4315
|
+
);
|
|
4316
|
+
const properties: Record<string, unknown> = {};
|
|
4317
|
+
const required: string[] = [];
|
|
4318
|
+
|
|
4319
|
+
for (const [propertyName, propertySchema] of Object.entries(result.properties)) {
|
|
4320
|
+
const normalizedProperty = normalizeAnthropicStrictSchemaNode(propertySchema, budget, cache);
|
|
4321
|
+
if (normalizedProperty === undefined) return undefined;
|
|
4322
|
+
|
|
4323
|
+
if (originalRequired.has(propertyName)) {
|
|
4324
|
+
properties[propertyName] = normalizedProperty;
|
|
4325
|
+
required.push(propertyName);
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
if (budget.optionalRemaining > 0) {
|
|
4330
|
+
budget.optionalRemaining--;
|
|
4331
|
+
budget.optionalCount++;
|
|
4332
|
+
properties[propertyName] = normalizedProperty;
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
const nullableProperty = makeAnthropicNullableSchema(normalizedProperty, budget);
|
|
4337
|
+
if (nullableProperty === undefined) return undefined;
|
|
4338
|
+
properties[propertyName] = nullableProperty;
|
|
4339
|
+
required.push(propertyName);
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
result.properties = properties;
|
|
4343
|
+
result.required = required;
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
if (Array.isArray(result.items)) {
|
|
4347
|
+
const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
|
|
4348
|
+
if (items === undefined) return undefined;
|
|
4349
|
+
result.items = items;
|
|
4350
|
+
} else if (isRecord(result.items)) {
|
|
4351
|
+
const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
|
|
4352
|
+
if (items === undefined) return undefined;
|
|
4353
|
+
result.items = items;
|
|
4354
|
+
}
|
|
4355
|
+
if (Array.isArray(result.prefixItems)) {
|
|
4356
|
+
const prefixItems = normalizeAnthropicStrictSchemaNode(result.prefixItems, budget, cache);
|
|
4357
|
+
if (prefixItems === undefined) return undefined;
|
|
4358
|
+
result.prefixItems = prefixItems;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
for (const key of COMBINATOR_KEYS) {
|
|
4362
|
+
const variants = result[key];
|
|
4363
|
+
if (!Array.isArray(variants)) continue;
|
|
4364
|
+
const normalizedVariants = normalizeAnthropicStrictSchemaNode(variants, budget, cache);
|
|
4365
|
+
if (normalizedVariants === undefined) return undefined;
|
|
4366
|
+
result[key] = normalizedVariants;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
for (const defsKey of ["$defs", "definitions"] as const) {
|
|
4370
|
+
const definitions = result[defsKey];
|
|
4371
|
+
if (!isRecord(definitions)) continue;
|
|
4372
|
+
const normalizedDefinitions: Record<string, unknown> = {};
|
|
4373
|
+
for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
|
|
4374
|
+
const normalizedDefinition = normalizeAnthropicStrictSchemaNode(definitionSchema, budget, cache);
|
|
4375
|
+
if (normalizedDefinition === undefined) return undefined;
|
|
4376
|
+
normalizedDefinitions[definitionName] = normalizedDefinition;
|
|
4377
|
+
}
|
|
4378
|
+
result[defsKey] = normalizedDefinitions;
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
return result;
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
const ANTHROPIC_STRICT_INCOMPATIBLE_KEYWORDS = [
|
|
4385
|
+
"oneOf",
|
|
4386
|
+
"allOf",
|
|
4387
|
+
"$ref",
|
|
4388
|
+
"patternProperties",
|
|
4389
|
+
"propertyNames",
|
|
4390
|
+
] as const;
|
|
4391
|
+
|
|
4392
|
+
/**
|
|
4393
|
+
* Anthropic's strict grammar subset supports anyOf/type-array unions only.
|
|
4394
|
+
* oneOf/allOf/$ref compile unpredictably (rejections arrive as 400s the
|
|
4395
|
+
* grammar-too-large fallback does not recognize, so they would hard-fail the
|
|
4396
|
+
* turn), and patternProperties/propertyNames describe open key sets that the
|
|
4397
|
+
* strict pipeline's injected `additionalProperties: false` would contradict.
|
|
4398
|
+
* Runs against the raw wire schema — the base normalizer spills several of
|
|
4399
|
+
* these keywords into the description, erasing the evidence.
|
|
4400
|
+
*/
|
|
4401
|
+
function hasAnthropicStrictIncompatibleKeyword(schema: unknown, seen = new Set<object>()): boolean {
|
|
4402
|
+
if (Array.isArray(schema)) {
|
|
4403
|
+
if (seen.has(schema)) return false;
|
|
4404
|
+
seen.add(schema);
|
|
4405
|
+
return schema.some(entry => hasAnthropicStrictIncompatibleKeyword(entry, seen));
|
|
4406
|
+
}
|
|
4407
|
+
if (!isRecord(schema)) return false;
|
|
4408
|
+
if (seen.has(schema)) return false;
|
|
4409
|
+
seen.add(schema);
|
|
4410
|
+
for (const keyword of ANTHROPIC_STRICT_INCOMPATIBLE_KEYWORDS) {
|
|
4411
|
+
if (schema[keyword] !== undefined) return true;
|
|
4412
|
+
}
|
|
4413
|
+
return Object.values(schema).some(value => hasAnthropicStrictIncompatibleKeyword(value, seen));
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
function normalizeAnthropicStrictSchema(
|
|
4417
|
+
schema: Record<string, unknown>,
|
|
4418
|
+
optionalRemaining: number,
|
|
4419
|
+
unionRemaining: number,
|
|
4420
|
+
): { schema: Record<string, unknown>; optionalCount: number; unionCount: number } | undefined {
|
|
4421
|
+
const budget: AnthropicStrictBudget = {
|
|
4422
|
+
optionalRemaining,
|
|
4423
|
+
unionRemaining,
|
|
4424
|
+
optionalCount: 0,
|
|
4425
|
+
unionCount: 0,
|
|
4426
|
+
};
|
|
4427
|
+
const normalized = normalizeAnthropicStrictSchemaNode(schema, budget, new WeakMap());
|
|
4428
|
+
if (!isRecord(normalized)) return undefined;
|
|
4429
|
+
return { schema: normalized, optionalCount: budget.optionalCount, unionCount: budget.unionCount };
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
|
|
4433
|
+
const jsonSchema = toolWireSchema(tool);
|
|
4434
|
+
return normalizeAnthropicToolSchema({
|
|
4435
|
+
...jsonSchema,
|
|
4436
|
+
type: "object",
|
|
4437
|
+
properties: isRecord(jsonSchema.properties) ? jsonSchema.properties : {},
|
|
4438
|
+
required: Array.isArray(jsonSchema.required)
|
|
4439
|
+
? jsonSchema.required.filter((entry): entry is string => typeof entry === "string")
|
|
4440
|
+
: [],
|
|
4441
|
+
}) as Record<string, unknown>;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function buildAnthropicToolSchemaPlans(tools: Tool[], disableStrictTools = false): AnthropicToolSchemaPlan[] {
|
|
4445
|
+
const plans = tools.map(
|
|
4446
|
+
(tool): AnthropicToolSchemaPlan => ({
|
|
4447
|
+
inputSchema: buildAnthropicBaseToolInputSchema(tool) as AnthropicToolInputSchema,
|
|
4448
|
+
strict: false,
|
|
4449
|
+
}),
|
|
4450
|
+
);
|
|
4451
|
+
if (NO_STRICT || disableStrictTools) return plans;
|
|
4452
|
+
|
|
4453
|
+
const candidateIndexes = tools.flatMap((tool, index) => {
|
|
4454
|
+
if (!ANTHROPIC_STRICT_TOOL_ALLOWLIST.has(tool.name)) return [];
|
|
4455
|
+
if (tool.strict === false) return [];
|
|
4456
|
+
if (hasAnthropicStrictIncompatibleKeyword(toolWireSchema(tool))) return [];
|
|
4457
|
+
return [index];
|
|
4458
|
+
});
|
|
4459
|
+
|
|
4460
|
+
let strictToolCount = 0;
|
|
4461
|
+
let strictOptionalParameterCount = 0;
|
|
4462
|
+
let strictUnionParameterCount = 0;
|
|
4463
|
+
for (const index of candidateIndexes) {
|
|
4464
|
+
if (strictToolCount >= MAX_ANTHROPIC_STRICT_TOOLS) break;
|
|
4465
|
+
|
|
4466
|
+
const strictResult = normalizeAnthropicStrictSchema(
|
|
4467
|
+
plans[index].inputSchema as Record<string, unknown>,
|
|
4468
|
+
MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS - strictOptionalParameterCount,
|
|
4469
|
+
MAX_ANTHROPIC_STRICT_UNION_PARAMETERS - strictUnionParameterCount,
|
|
4470
|
+
);
|
|
4471
|
+
if (!strictResult) continue;
|
|
4472
|
+
|
|
4473
|
+
plans[index] = {
|
|
4474
|
+
inputSchema: strictResult.schema as AnthropicToolInputSchema,
|
|
4475
|
+
strict: true,
|
|
4476
|
+
};
|
|
4477
|
+
strictToolCount++;
|
|
4478
|
+
strictOptionalParameterCount += strictResult.optionalCount;
|
|
4479
|
+
strictUnionParameterCount += strictResult.unionCount;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
return plans;
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
function convertTools(
|
|
4486
|
+
tools: Tool[],
|
|
4487
|
+
isOAuthToken: boolean,
|
|
4488
|
+
disableStrictTools = false,
|
|
4489
|
+
supportsEagerToolInputStreaming = true,
|
|
4490
|
+
escapeBuiltinToolNames = false,
|
|
4491
|
+
useUmansGatewayWebSearch = false,
|
|
4492
|
+
): AnthropicWireTool[] {
|
|
4493
|
+
if (!tools) return [];
|
|
4494
|
+
const schemaPlans = buildAnthropicToolSchemaPlans(tools, disableStrictTools);
|
|
4495
|
+
|
|
4496
|
+
return tools.map((tool, index) => {
|
|
4497
|
+
const plan = schemaPlans[index];
|
|
4498
|
+
const baseTool = {
|
|
4499
|
+
name: encodeAnthropicToolName(tool.name, isOAuthToken, escapeBuiltinToolNames, useUmansGatewayWebSearch),
|
|
4500
|
+
description: tool.description || "",
|
|
4501
|
+
input_schema: plan.inputSchema,
|
|
4502
|
+
};
|
|
4503
|
+
return {
|
|
4504
|
+
...baseTool,
|
|
4505
|
+
...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
|
|
4506
|
+
...(plan.strict ? { strict: true } : {}),
|
|
4507
|
+
};
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
function mapStopReason(reason: string): StopReason {
|
|
4512
|
+
switch (reason) {
|
|
4513
|
+
case "end_turn":
|
|
4514
|
+
return "stop";
|
|
4515
|
+
case "max_tokens":
|
|
4516
|
+
return "length";
|
|
4517
|
+
// Generation ran into the model's context window (default behavior on
|
|
4518
|
+
// Sonnet 4.5+); the streamed content is valid, just truncated.
|
|
4519
|
+
case "model_context_window_exceeded":
|
|
4520
|
+
return "length";
|
|
4521
|
+
case "tool_use":
|
|
4522
|
+
return "toolUse";
|
|
4523
|
+
case "refusal":
|
|
4524
|
+
return "error";
|
|
4525
|
+
case "pause_turn": // Stop is good enough -> resubmit
|
|
4526
|
+
return "stop";
|
|
4527
|
+
case "stop_sequence":
|
|
4528
|
+
return "stop"; // A caller-supplied stop_sequences entry matched; the turn completed normally.
|
|
4529
|
+
case "sensitive": // Content flagged by safety filters (not yet in SDK types)
|
|
4530
|
+
return "error";
|
|
4531
|
+
default:
|
|
4532
|
+
// New stop reasons ship server-side first ("sensitive",
|
|
4533
|
+
// "model_context_window_exceeded") and arrive on the trailing
|
|
4534
|
+
// message_delta after all content has streamed. Degrade to a normal
|
|
4535
|
+
// stop instead of failing the fully streamed turn.
|
|
4536
|
+
reportAnthropicEnvelopeAnomaly(`unhandled stop reason: ${reason}`);
|
|
4537
|
+
return "stop";
|
|
4538
|
+
}
|
|
4539
|
+
}
|