@vib-rato/ai 0.16.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 +3354 -0
- package/README.md +1194 -0
- package/dist/types/adapter-internals/provider-safety-stop.d.ts +45 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +80 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/redact.d.ts +7 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +153 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +132 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +555 -0
- package/dist/types/auth-gateway/http.d.ts +40 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +70 -0
- package/dist/types/auth-gateway/types.d.ts +129 -0
- package/dist/types/auth-storage.d.ts +1074 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/codex-tools.d.ts +4 -0
- package/dist/types/context-cap-policy.d.ts +68 -0
- package/dist/types/core.d.ts +35 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/model-cache.d.ts +24 -0
- package/dist/types/model-manager.d.ts +77 -0
- package/dist/types/model-pricing.d.ts +3 -0
- package/dist/types/model-retirements.d.ts +6 -0
- package/dist/types/model-thinking.d.ts +100 -0
- package/dist/types/models.d.ts +21 -0
- package/dist/types/openai-completions-compat.d.ts +34 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +293 -0
- package/dist/types/provider-models/special.d.ts +29 -0
- package/dist/types/providers/amazon-bedrock.d.ts +60 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +280 -0
- package/dist/types/providers/aws-credential-config.d.ts +19 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +22 -0
- package/dist/types/providers/composer-discipline.d.ts +32 -0
- package/dist/types/providers/cursor/client-version.d.ts +10 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +16769 -0
- package/dist/types/providers/cursor-pi-args.d.ts +119 -0
- package/dist/types/providers/cursor.d.ts +72 -0
- package/dist/types/providers/dashscope-token-plan-headers.d.ts +57 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +26 -0
- package/dist/types/providers/google-gemini-cli.d.ts +75 -0
- package/dist/types/providers/google-gemini-headers.d.ts +43 -0
- package/dist/types/providers/google-shared.d.ts +183 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +11 -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/kiro-api-key.d.ts +50 -0
- package/dist/types/providers/kiro-codewhisperer.d.ts +11 -0
- package/dist/types/providers/mock.d.ts +189 -0
- package/dist/types/providers/ollama.d.ts +41 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-bounded-rate-limits.d.ts +3 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +1733 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +50 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +18 -0
- package/dist/types/providers/openai-codex-responses.d.ts +71 -0
- package/dist/types/providers/openai-completions-compat.d.ts +6 -0
- package/dist/types/providers/openai-completions.d.ts +35 -0
- package/dist/types/providers/openai-opencodex-responses.d.ts +10 -0
- package/dist/types/providers/openai-request-transform.d.ts +4 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +106 -0
- package/dist/types/providers/openai-responses.d.ts +37 -0
- package/dist/types/providers/pi-native-client.d.ts +29 -0
- package/dist/types/providers/pi-native-server.d.ts +60 -0
- package/dist/types/providers/register-builtins.d.ts +59 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +33 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +45 -0
- package/dist/types/types.d.ts +1073 -0
- package/dist/types/usage/claude.d.ts +3 -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 +2 -0
- package/dist/types/usage/grok-cli.d.ts +17 -0
- package/dist/types/usage/kimi.d.ts +4 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +264 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +39 -0
- package/dist/types/utils/block-symbols.d.ts +6 -0
- package/dist/types/utils/discovery/antigravity.d.ts +67 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +49 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +81 -0
- package/dist/types/utils/event-stream.d.ts +41 -0
- package/dist/types/utils/fallback-transport.d.ts +110 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +11 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +59 -0
- package/dist/types/utils/idle-iterator.d.ts +122 -0
- package/dist/types/utils/json-parse.d.ts +98 -0
- package/dist/types/utils/oauth/alibaba-token-plan.d.ts +19 -0
- package/dist/types/utils/oauth/anthropic.d.ts +41 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +38 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +39 -0
- package/dist/types/utils/oauth/bizrouter.d.ts +1 -0
- package/dist/types/utils/oauth/callback-server.d.ts +80 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/commandcode.d.ts +1 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepinfra.d.ts +1 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/fugu.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/glm-zcode.d.ts +71 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +39 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +17 -0
- package/dist/types/utils/oauth/kiro.d.ts +71 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/mara.d.ts +1 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/opengateway.d.ts +1 -0
- package/dist/types/utils/oauth/openrouter.d.ts +1 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +4 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/sglang.d.ts +16 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +56 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xai.d.ts +30 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +25 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +14 -0
- package/dist/types/utils/parse-bind.d.ts +26 -0
- package/dist/types/utils/provider-response.d.ts +6 -0
- package/dist/types/utils/provider-safety-stop.d.ts +8 -0
- package/dist/types/utils/proxy.d.ts +7 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry-budget.d.ts +1 -0
- package/dist/types/utils/retry.d.ts +29 -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 +49 -0
- package/dist/types/utils/schema/index.d.ts +14 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/root-combinator.d.ts +12 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +80 -0
- package/dist/types/utils/tool-choice-capability.d.ts +57 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +117 -0
- package/package.json +152 -0
- package/src/adapter-internals/provider-safety-stop.d.ts +45 -0
- package/src/adapter-internals/provider-safety-stop.ts +156 -0
- package/src/api-registry.d.ts +30 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +444 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/redact.ts +39 -0
- package/src/auth-broker/refresher.ts +130 -0
- package/src/auth-broker/remote-store.ts +1576 -0
- package/src/auth-broker/server.ts +764 -0
- package/src/auth-broker/types.ts +164 -0
- package/src/auth-broker/wire-schemas.ts +261 -0
- package/src/auth-gateway/http.ts +198 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +1315 -0
- package/src/auth-gateway/types.ts +160 -0
- package/src/auth-storage.ts +7312 -0
- package/src/cli.ts +269 -0
- package/src/codex-tools.d.ts +4 -0
- package/src/codex-tools.ts +24 -0
- package/src/context-cap-policy.d.ts +68 -0
- package/src/context-cap-policy.ts +123 -0
- package/src/core.ts +44 -0
- package/src/index.ts +61 -0
- package/src/model-cache.ts +236 -0
- package/src/model-manager.ts +744 -0
- package/src/model-pricing.d.ts +3 -0
- package/src/model-pricing.ts +68 -0
- package/src/model-retirements.d.ts +6 -0
- package/src/model-retirements.ts +19 -0
- package/src/model-thinking.d.ts +100 -0
- package/src/model-thinking.ts +1054 -0
- package/src/models.d.ts +21 -0
- package/src/models.json +94672 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +126 -0
- package/src/openai-completions-compat.d.ts +34 -0
- package/src/openai-completions-compat.ts +383 -0
- package/src/prompts/composer-bash-policy-recovery.md +1 -0
- package/src/prompts/cursor-composer-bash-policy-recovery.md +1 -0
- package/src/prompts/cursor-composer-edit-discipline.md +7 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +392 -0
- package/src/provider-models/google.ts +92 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +159 -0
- package/src/provider-models/openai-compat.ts +2920 -0
- package/src/provider-models/special.ts +185 -0
- package/src/providers/amazon-bedrock.d.ts +60 -0
- package/src/providers/amazon-bedrock.ts +939 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +839 -0
- package/src/providers/anthropic.d.ts +280 -0
- package/src/providers/anthropic.ts +4421 -0
- package/src/providers/aws-credential-config.d.ts +19 -0
- package/src/providers/aws-credential-config.ts +179 -0
- package/src/providers/aws-credentials.d.ts +43 -0
- package/src/providers/aws-credentials.ts +457 -0
- package/src/providers/aws-eventstream.d.ts +38 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.d.ts +55 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.d.ts +22 -0
- package/src/providers/azure-openai-responses.ts +447 -0
- package/src/providers/composer-discipline.d.ts +32 -0
- package/src/providers/composer-discipline.ts +95 -0
- package/src/providers/cursor/client-version.d.ts +10 -0
- package/src/providers/cursor/client-version.ts +10 -0
- package/src/providers/cursor/exec-modern.d.ts +98 -0
- package/src/providers/cursor/exec-modern.ts +497 -0
- package/src/providers/cursor/gen/agent_pb.d.ts +16769 -0
- package/src/providers/cursor/gen/agent_pb.ts +19780 -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.d.ts +119 -0
- package/src/providers/cursor-pi-args.ts +187 -0
- package/src/providers/cursor.d.ts +72 -0
- package/src/providers/cursor.ts +3396 -0
- package/src/providers/dashscope-token-plan-headers.d.ts +57 -0
- package/src/providers/dashscope-token-plan-headers.ts +84 -0
- package/src/providers/error-message.d.ts +27 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.d.ts +40 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.d.ts +27 -0
- package/src/providers/gitlab-duo.ts +393 -0
- package/src/providers/google-auth.d.ts +26 -0
- package/src/providers/google-auth.ts +262 -0
- package/src/providers/google-gemini-cli.d.ts +75 -0
- package/src/providers/google-gemini-cli.ts +969 -0
- package/src/providers/google-gemini-headers.d.ts +43 -0
- package/src/providers/google-gemini-headers.ts +100 -0
- package/src/providers/google-shared.d.ts +183 -0
- package/src/providers/google-shared.ts +1105 -0
- package/src/providers/google-types.d.ts +138 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.d.ts +11 -0
- package/src/providers/google-vertex.ts +124 -0
- package/src/providers/google.d.ts +4 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.d.ts +1 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.d.ts +27 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/kiro-api-key.d.ts +50 -0
- package/src/providers/kiro-api-key.ts +786 -0
- package/src/providers/kiro-codewhisperer.d.ts +11 -0
- package/src/providers/kiro-codewhisperer.ts +600 -0
- package/src/providers/mock.ts +526 -0
- package/src/providers/ollama.d.ts +41 -0
- package/src/providers/ollama.ts +645 -0
- package/src/providers/openai-anthropic-shim.d.ts +31 -0
- package/src/providers/openai-anthropic-shim.ts +156 -0
- package/src/providers/openai-bounded-rate-limits.d.ts +3 -0
- package/src/providers/openai-bounded-rate-limits.ts +57 -0
- package/src/providers/openai-chat-server-schema.ts +254 -0
- package/src/providers/openai-chat-server.ts +724 -0
- package/src/providers/openai-codex/constants.d.ts +26 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.d.ts +50 -0
- package/src/providers/openai-codex/request-transformer.ts +219 -0
- package/src/providers/openai-codex/response-handler.d.ts +18 -0
- package/src/providers/openai-codex/response-handler.ts +111 -0
- package/src/providers/openai-codex-responses.d.ts +71 -0
- package/src/providers/openai-codex-responses.ts +3288 -0
- package/src/providers/openai-completions-compat.d.ts +6 -0
- package/src/providers/openai-completions-compat.ts +6 -0
- package/src/providers/openai-completions.d.ts +35 -0
- package/src/providers/openai-completions.ts +2294 -0
- package/src/providers/openai-opencodex-responses.ts +174 -0
- package/src/providers/openai-request-transform.d.ts +4 -0
- package/src/providers/openai-request-transform.ts +136 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1268 -0
- package/src/providers/openai-responses-shared.d.ts +106 -0
- package/src/providers/openai-responses-shared.ts +1253 -0
- package/src/providers/openai-responses.d.ts +37 -0
- package/src/providers/openai-responses.ts +989 -0
- package/src/providers/pi-native-client.d.ts +29 -0
- package/src/providers/pi-native-client.ts +243 -0
- package/src/providers/pi-native-server.ts +488 -0
- package/src/providers/register-builtins.d.ts +59 -0
- package/src/providers/register-builtins.ts +544 -0
- package/src/providers/synthetic.d.ts +26 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.d.ts +33 -0
- package/src/providers/transform-messages.ts +408 -0
- package/src/providers/vision-guard.d.ts +8 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.d.ts +19 -0
- package/src/rate-limit-utils.ts +102 -0
- package/src/stream.d.ts +45 -0
- package/src/stream.ts +1306 -0
- package/src/types.d.ts +1073 -0
- package/src/types.ts +1305 -0
- package/src/usage/claude.ts +449 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/grok-cli.ts +259 -0
- package/src/usage/kimi.ts +285 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +190 -0
- package/src/utils/abort.d.ts +19 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +95 -0
- package/src/utils/block-symbols.d.ts +6 -0
- package/src/utils/block-symbols.ts +11 -0
- package/src/utils/discovery/antigravity.ts +275 -0
- package/src/utils/discovery/codex.ts +362 -0
- package/src/utils/discovery/cursor.ts +388 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +379 -0
- package/src/utils/event-stream.d.ts +41 -0
- package/src/utils/event-stream.ts +269 -0
- package/src/utils/fallback-transport.d.ts +110 -0
- package/src/utils/fallback-transport.ts +411 -0
- package/src/utils/fireworks-model-id.d.ts +10 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.d.ts +11 -0
- package/src/utils/foundry.ts +18 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.d.ts +59 -0
- package/src/utils/http-inspector.ts +380 -0
- package/src/utils/idle-iterator.d.ts +122 -0
- package/src/utils/idle-iterator.ts +410 -0
- package/src/utils/json-parse.d.ts +98 -0
- package/src/utils/json-parse.ts +607 -0
- package/src/utils/oauth/alibaba-token-plan.ts +60 -0
- package/src/utils/oauth/anthropic.ts +233 -0
- package/src/utils/oauth/api-key-login.ts +98 -0
- package/src/utils/oauth/api-key-validation.ts +344 -0
- package/src/utils/oauth/bizrouter.ts +15 -0
- package/src/utils/oauth/callback-server.d.ts +80 -0
- package/src/utils/oauth/callback-server.ts +359 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/commandcode.ts +17 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepinfra.ts +15 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/fugu.ts +15 -0
- package/src/utils/oauth/github-copilot.d.ts +38 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/glm-zcode.d.ts +71 -0
- package/src/utils/oauth/glm-zcode.ts +433 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +558 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.d.ts +17 -0
- package/src/utils/oauth/kimi.ts +275 -0
- package/src/utils/oauth/kiro.ts +448 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/mara.ts +16 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/opengateway.ts +15 -0
- package/src/utils/oauth/openrouter.ts +16 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +225 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/sglang.ts +42 -0
- package/src/utils/oauth/synthetic.ts +15 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.d.ts +56 -0
- package/src/utils/oauth/types.ts +122 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +42 -0
- package/src/utils/oauth/xai.ts +246 -0
- package/src/utils/oauth/xiaomi.ts +199 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +275 -0
- package/src/utils/parse-bind.ts +81 -0
- package/src/utils/provider-response.d.ts +6 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/provider-safety-stop.ts +8 -0
- package/src/utils/proxy.d.ts +7 -0
- package/src/utils/proxy.ts +652 -0
- package/src/utils/retry-after.d.ts +3 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry-budget.d.ts +1 -0
- package/src/utils/retry-budget.ts +4 -0
- package/src/utils/retry.d.ts +29 -0
- package/src/utils/retry.ts +67 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.d.ts +24 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.d.ts +30 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.d.ts +11 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.d.ts +10 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.d.ts +4 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.d.ts +49 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.d.ts +14 -0
- package/src/utils/schema/index.ts +14 -0
- package/src/utils/schema/json-schema-validator.d.ts +12 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.d.ts +2 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.d.ts +93 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/root-combinator.d.ts +12 -0
- package/src/utils/schema/root-combinator.ts +143 -0
- package/src/utils/schema/spill.d.ts +8 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.d.ts +25 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.d.ts +4 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.d.ts +54 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.d.ts +31 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.d.ts +10 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.d.ts +80 -0
- package/src/utils/tool-call-healing.ts +298 -0
- package/src/utils/tool-choice-capability.d.ts +57 -0
- package/src/utils/tool-choice-capability.ts +633 -0
- package/src/utils/tool-choice.d.ts +50 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1080 -0
- package/src/utils.d.ts +117 -0
- package/src/utils.ts +523 -0
|
@@ -0,0 +1,4421 @@
|
|
|
1
|
+
import * as nodeCrypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { scheduler } from "node:timers/promises";
|
|
5
|
+
import * as tls from "node:tls";
|
|
6
|
+
import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
|
|
7
|
+
import type {
|
|
8
|
+
ContentBlockParam,
|
|
9
|
+
MessageCreateParamsStreaming,
|
|
10
|
+
MessageParam,
|
|
11
|
+
RawMessageStreamEvent,
|
|
12
|
+
} from "@anthropic-ai/sdk/resources/messages";
|
|
13
|
+
import {
|
|
14
|
+
$credentialEnv,
|
|
15
|
+
$env,
|
|
16
|
+
extractHttpStatusFromError,
|
|
17
|
+
isEnoent,
|
|
18
|
+
isRetryableError,
|
|
19
|
+
isUnexpectedSocketCloseMessage,
|
|
20
|
+
logger,
|
|
21
|
+
readSseEvents,
|
|
22
|
+
} from "@vib-rato/utils";
|
|
23
|
+
import {
|
|
24
|
+
isProviderSafetyStopAdapterInvocation,
|
|
25
|
+
mintProviderSafetyStop,
|
|
26
|
+
PROVIDER_SAFETY_STOP_ADAPTER_CAPABILITY,
|
|
27
|
+
} from "../adapter-internals/provider-safety-stop";
|
|
28
|
+
import {
|
|
29
|
+
hasOpus47ApiRestrictions,
|
|
30
|
+
mapEffortToAnthropicAdaptiveEffort,
|
|
31
|
+
supportsAnthropicAdaptiveThinkingDisplay as supportsAdaptiveThinkingDisplay,
|
|
32
|
+
} from "../model-thinking";
|
|
33
|
+
import { calculateCost } from "../models";
|
|
34
|
+
import { isUsageLimitError } from "../rate-limit-utils";
|
|
35
|
+
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
36
|
+
import type {
|
|
37
|
+
Api,
|
|
38
|
+
AssistantMessage,
|
|
39
|
+
CacheRetention,
|
|
40
|
+
Context,
|
|
41
|
+
FetchImpl,
|
|
42
|
+
ImageContent,
|
|
43
|
+
Message,
|
|
44
|
+
Model,
|
|
45
|
+
ProviderSessionState,
|
|
46
|
+
RedactedThinkingContent,
|
|
47
|
+
ServiceTier,
|
|
48
|
+
SimpleStreamOptions,
|
|
49
|
+
StopReason,
|
|
50
|
+
StreamFunction,
|
|
51
|
+
StreamOptions,
|
|
52
|
+
TextContent,
|
|
53
|
+
ThinkingContent,
|
|
54
|
+
Tool,
|
|
55
|
+
ToolCall,
|
|
56
|
+
ToolResultMessage,
|
|
57
|
+
Usage,
|
|
58
|
+
} from "../types";
|
|
59
|
+
import { resolveServiceTier } from "../types";
|
|
60
|
+
import {
|
|
61
|
+
isAnthropicOAuthToken,
|
|
62
|
+
isRecord,
|
|
63
|
+
normalizeSystemPrompts,
|
|
64
|
+
normalizeToolCallId,
|
|
65
|
+
resolveCacheRetention,
|
|
66
|
+
sanitizeJsonStrings,
|
|
67
|
+
} from "../utils";
|
|
68
|
+
import { createAbortSourceTracker } from "../utils/abort";
|
|
69
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
70
|
+
import { transportFailureFacts } from "../utils/fallback-transport";
|
|
71
|
+
import { isFoundryEnabled } from "../utils/foundry";
|
|
72
|
+
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
73
|
+
import {
|
|
74
|
+
FirstEventTimeoutError,
|
|
75
|
+
getProviderFirstEventTimeoutFallbackMs,
|
|
76
|
+
getProviderStreamIdleTimeoutFallbackMs,
|
|
77
|
+
getStreamFirstEventTimeoutMs,
|
|
78
|
+
getStreamIdleTimeoutMs,
|
|
79
|
+
iterateWithIdleTimeout,
|
|
80
|
+
resolveAnthropicSdkRequestTimeoutMs,
|
|
81
|
+
} from "../utils/idle-iterator";
|
|
82
|
+
import {
|
|
83
|
+
captureUnicodeEscapeEvidence,
|
|
84
|
+
isCompleteJson,
|
|
85
|
+
parseJsonWithRepair,
|
|
86
|
+
parseStreamingJson,
|
|
87
|
+
} from "../utils/json-parse";
|
|
88
|
+
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
89
|
+
import { GLM_ZCODE_ANTHROPIC_BASE_URL } from "../utils/oauth/glm-zcode";
|
|
90
|
+
import { notifyProviderResponse } from "../utils/provider-response";
|
|
91
|
+
import { isCopilotTransientModelError } from "../utils/retry";
|
|
92
|
+
import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
93
|
+
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
94
|
+
import {
|
|
95
|
+
COMBINATOR_KEYS,
|
|
96
|
+
flattenToolRootCombinators,
|
|
97
|
+
isJsonSchemaObjectNode,
|
|
98
|
+
NO_STRICT,
|
|
99
|
+
toolWireSchema,
|
|
100
|
+
} from "../utils/schema";
|
|
101
|
+
import { spillToDescription } from "../utils/schema/spill";
|
|
102
|
+
import { notifyRawSseEvent, wrapFetchForSseDebug } from "../utils/sse-debug";
|
|
103
|
+
import {
|
|
104
|
+
isForcedToolChoiceUnsupportedError,
|
|
105
|
+
markToolChoiceIncapability,
|
|
106
|
+
type ResolveToolChoiceResult,
|
|
107
|
+
resolveToolChoice,
|
|
108
|
+
} from "../utils/tool-choice-capability";
|
|
109
|
+
import {
|
|
110
|
+
buildCopilotDynamicHeaders,
|
|
111
|
+
hasCopilotVisionInput,
|
|
112
|
+
resolveGitHubCopilotBaseUrl,
|
|
113
|
+
} from "./github-copilot-headers";
|
|
114
|
+
import { hasAdjacentPrivateThinkingBlocks, transformMessages } from "./transform-messages";
|
|
115
|
+
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
116
|
+
|
|
117
|
+
export type AnthropicHeaderOptions = {
|
|
118
|
+
apiKey: string;
|
|
119
|
+
baseUrl?: string;
|
|
120
|
+
isOAuth?: boolean;
|
|
121
|
+
extraBetas?: string[];
|
|
122
|
+
stream?: boolean;
|
|
123
|
+
modelHeaders?: Record<string, string>;
|
|
124
|
+
isCloudflareAiGateway?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Attach ZCode client "source" headers (User-Agent: ZCode/<ver>, X-Title,
|
|
127
|
+
* X-ZCode-Agent: glm, X-Platform, etc.) so api.z.ai recognizes the caller as
|
|
128
|
+
* the ZCode client, exactly like ZCode's `buildZCodeSourceHeaders` does for
|
|
129
|
+
* GLM providers. glm-zcode only.
|
|
130
|
+
*/
|
|
131
|
+
zcodeSourceHeaders?: boolean;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
|
|
135
|
+
const trimmed = baseUrl?.trim();
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
|
|
140
|
+
return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build deduplicated beta header string
|
|
144
|
+
export function buildBetaHeader(baseBetas: string[], extraBetas: string[]): string {
|
|
145
|
+
const seen = new Set<string>();
|
|
146
|
+
const result: string[] = [];
|
|
147
|
+
for (const beta of [...baseBetas, ...extraBetas]) {
|
|
148
|
+
const trimmed = beta.trim();
|
|
149
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
150
|
+
seen.add(trimmed);
|
|
151
|
+
result.push(trimmed);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result.join(",");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const claudeCodeBetaDefaults = [
|
|
158
|
+
"claude-code-20250219",
|
|
159
|
+
"oauth-2025-04-20",
|
|
160
|
+
"context-management-2025-06-27",
|
|
161
|
+
"prompt-caching-scope-2026-01-05",
|
|
162
|
+
];
|
|
163
|
+
const fineGrainedToolStreamingBeta = "fine-grained-tool-streaming-2025-05-14";
|
|
164
|
+
const interleavedThinkingBeta = "interleaved-thinking-2025-05-14";
|
|
165
|
+
const fastModeBeta = "fast-mode-2026-02-01";
|
|
166
|
+
|
|
167
|
+
function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined {
|
|
168
|
+
if (!headers) return undefined;
|
|
169
|
+
const normalizedName = headerName.toLowerCase();
|
|
170
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
171
|
+
if (key.toLowerCase() === normalizedName) return value;
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isClaudeCodeClientUserAgent(userAgent: string | undefined): userAgent is string {
|
|
177
|
+
if (!userAgent) return false;
|
|
178
|
+
return userAgent.toLowerCase().startsWith("claude-cli");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isAnthropicApiBaseUrl(baseUrl?: string): boolean {
|
|
182
|
+
if (!baseUrl) return true;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(baseUrl);
|
|
185
|
+
return url.protocol.toLowerCase() === "https:" && url.hostname.toLowerCase() === "api.anthropic.com";
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sharedHeaders = {
|
|
192
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
193
|
+
Connection: "keep-alive",
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"Anthropic-Version": "2023-06-01",
|
|
196
|
+
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
197
|
+
"X-App": "cli",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ZCode bakes its app version and runtime env at build time. Mirror the values
|
|
201
|
+
// from the analyzed ZCode 3.1.2 desktop bundle (`resolveRuntimeZCodeEnv` returns
|
|
202
|
+
// "production" for non-test builds). Both are overridable for forward-compat.
|
|
203
|
+
const ZCODE_APP_VERSION = process.env.ZCODE_APP_VERSION?.trim() || "3.1.2";
|
|
204
|
+
const ZCODE_RELEASE_CHANNEL = process.env.ZCODE_RELEASE_CHANNEL?.trim() || "production";
|
|
205
|
+
|
|
206
|
+
// Mirrors ZCode's `normalizePrintableHeaderValue`: only printable ASCII passes.
|
|
207
|
+
function normalizePrintableHeaderValue(value: string | undefined): string | undefined {
|
|
208
|
+
const trimmed = value?.trim();
|
|
209
|
+
if (trimmed && /^[\x20-\x7e]+$/.test(trimmed)) return trimmed;
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Mirrors ZCode's `normalizeOsCategory`.
|
|
214
|
+
function normalizeOsCategory(platform: NodeJS.Platform): string {
|
|
215
|
+
switch (platform) {
|
|
216
|
+
case "darwin":
|
|
217
|
+
return "macos";
|
|
218
|
+
case "win32":
|
|
219
|
+
return "windows";
|
|
220
|
+
default:
|
|
221
|
+
return "linux";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Replicates ZCode's `buildZCodeSourceHeaders()` + GLM `X-ZCode-Agent` tag
|
|
227
|
+
* (host bundle `Bl` / `buildConnectivitySourceHeaders` for GLM providers), so
|
|
228
|
+
* api.z.ai sees vib's glm-zcode requests as the ZCode client. Dynamic values
|
|
229
|
+
* (platform/arch, locale, timezone, OS version) are resolved at runtime exactly
|
|
230
|
+
* as ZCode does; printable-ASCII-only and conditionally omitted when empty.
|
|
231
|
+
*/
|
|
232
|
+
export function buildZCodeSourceHeaders(): Record<string, string> {
|
|
233
|
+
const platform = process.platform;
|
|
234
|
+
const arch = process.arch;
|
|
235
|
+
const appVersion = normalizePrintableHeaderValue(ZCODE_APP_VERSION);
|
|
236
|
+
const releaseChannel = normalizePrintableHeaderValue(ZCODE_RELEASE_CHANNEL);
|
|
237
|
+
let locale: string | undefined;
|
|
238
|
+
let timezone: string | undefined;
|
|
239
|
+
try {
|
|
240
|
+
const resolved = Intl.DateTimeFormat().resolvedOptions();
|
|
241
|
+
locale = normalizePrintableHeaderValue(resolved.locale);
|
|
242
|
+
timezone = normalizePrintableHeaderValue(resolved.timeZone);
|
|
243
|
+
} catch {}
|
|
244
|
+
const osVersion = normalizePrintableHeaderValue(os.version());
|
|
245
|
+
const headers: Record<string, string> = {
|
|
246
|
+
"User-Agent": `ZCode/${appVersion ?? "unknown"}`,
|
|
247
|
+
"HTTP-Referer": "https://zcode.z.ai",
|
|
248
|
+
"X-Title": "Z Code@electron",
|
|
249
|
+
"X-Platform": `${platform}-${arch}`,
|
|
250
|
+
"X-Client-Language": locale ?? "unknown",
|
|
251
|
+
"X-Client-Timezone": timezone ?? "unknown",
|
|
252
|
+
"X-Os-Category": normalizeOsCategory(platform),
|
|
253
|
+
"X-ZCode-Agent": "glm",
|
|
254
|
+
};
|
|
255
|
+
if (appVersion) headers["X-ZCode-App-Version"] = appVersion;
|
|
256
|
+
if (releaseChannel) headers["X-Release-Channel"] = releaseChannel;
|
|
257
|
+
if (osVersion) headers["X-Os-Version"] = osVersion;
|
|
258
|
+
return headers;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
|
|
262
|
+
const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
|
|
263
|
+
const extraBetas = options.extraBetas ?? [];
|
|
264
|
+
const stream = options.stream ?? false;
|
|
265
|
+
const betaHeader = buildBetaHeader(claudeCodeBetaDefaults, extraBetas);
|
|
266
|
+
const acceptHeader = stream ? "text/event-stream" : "application/json";
|
|
267
|
+
const modelHeaders = Object.fromEntries(
|
|
268
|
+
Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (options.isCloudflareAiGateway) {
|
|
272
|
+
return {
|
|
273
|
+
...modelHeaders,
|
|
274
|
+
Accept: acceptHeader,
|
|
275
|
+
...sharedHeaders,
|
|
276
|
+
"Anthropic-Beta": betaHeader,
|
|
277
|
+
"cf-aig-authorization": `Bearer ${options.apiKey}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (oauthToken) {
|
|
282
|
+
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
283
|
+
const userAgent = isClaudeCodeClientUserAgent(incomingUserAgent)
|
|
284
|
+
? incomingUserAgent
|
|
285
|
+
: `claude-cli/${claudeCodeVersion} (external, cli)`;
|
|
286
|
+
return {
|
|
287
|
+
...modelHeaders,
|
|
288
|
+
...claudeCodeHeaders,
|
|
289
|
+
Accept: acceptHeader,
|
|
290
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
291
|
+
...sharedHeaders,
|
|
292
|
+
"Anthropic-Beta": betaHeader,
|
|
293
|
+
"User-Agent": userAgent,
|
|
294
|
+
};
|
|
295
|
+
} else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
|
|
296
|
+
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
297
|
+
// ZCode merges its source headers LAST for GLM providers (`withZCodeSourceHeaders`
|
|
298
|
+
// → `{ ...base, ...extra, ...source }`), so they win over any incoming User-Agent.
|
|
299
|
+
const zcodeSourceHeaders = options.zcodeSourceHeaders ? buildZCodeSourceHeaders() : undefined;
|
|
300
|
+
return {
|
|
301
|
+
...modelHeaders,
|
|
302
|
+
Accept: acceptHeader,
|
|
303
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
304
|
+
...sharedHeaders,
|
|
305
|
+
"Anthropic-Beta": betaHeader,
|
|
306
|
+
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
307
|
+
...(zcodeSourceHeaders ?? {}),
|
|
308
|
+
};
|
|
309
|
+
} else {
|
|
310
|
+
return {
|
|
311
|
+
...modelHeaders,
|
|
312
|
+
Accept: acceptHeader,
|
|
313
|
+
...sharedHeaders,
|
|
314
|
+
"Anthropic-Beta": betaHeader,
|
|
315
|
+
"X-Api-Key": options.apiKey,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type AnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" | "5m" };
|
|
321
|
+
|
|
322
|
+
type AnthropicSamplingParams = MessageCreateParamsStreaming & {
|
|
323
|
+
top_p?: number;
|
|
324
|
+
top_k?: number;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
|
|
328
|
+
let warnedStopSequencesTrim = false;
|
|
329
|
+
|
|
330
|
+
const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Scope of a classified replayed-thinking repair currently applied to this
|
|
334
|
+
* session: `latest` drops native thinking from the newest assistant turn, `all`
|
|
335
|
+
* stops replaying native thinking entirely. Persisted across stream
|
|
336
|
+
* re-invocations so a repair that keeps being rejected is not re-attempted from
|
|
337
|
+
* scratch on every turn (issue #4011), and released again by the first stream
|
|
338
|
+
* that completes. Unclassifiable masked `api_error` repairs remain local to the
|
|
339
|
+
* current stream invocation because a transient masked failure must not degrade
|
|
340
|
+
* later turns.
|
|
341
|
+
*/
|
|
342
|
+
type AnthropicThinkingReplayRepairScope = "none" | "latest" | "all";
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Repairs are bounded independently of `PROVIDER_MAX_RETRIES` because they do
|
|
346
|
+
* not consume the provider retry budget: without their own ceiling an
|
|
347
|
+
* unacceptable request shape retries forever (issue #4011). The ceiling spans
|
|
348
|
+
* the session rather than a single stream, and only a completed stream re-arms
|
|
349
|
+
* it — an unacceptable shape never completes, so it can never buy more repairs.
|
|
350
|
+
*/
|
|
351
|
+
const ANTHROPIC_MAX_THINKING_REPAIRS = 1;
|
|
352
|
+
|
|
353
|
+
type AnthropicPayloadFingerprint = {
|
|
354
|
+
sha256: string;
|
|
355
|
+
bytes: number;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type AnthropicThinkingRepairCandidate = {
|
|
359
|
+
scope: Exclude<AnthropicThinkingReplayRepairScope, "none">;
|
|
360
|
+
params: MessageCreateParamsStreaming;
|
|
361
|
+
fingerprint: AnthropicPayloadFingerprint;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
type AnthropicProviderSessionState = ProviderSessionState & {
|
|
365
|
+
strictToolsDisabled: boolean;
|
|
366
|
+
fastModeDisabled: boolean;
|
|
367
|
+
generatedCacheBudget: GeneratedCacheBudget;
|
|
368
|
+
thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope;
|
|
369
|
+
thinkingReplayRepairAttempts: number;
|
|
370
|
+
thinkingReplayRejectedPayload?: AnthropicPayloadFingerprint;
|
|
371
|
+
/**
|
|
372
|
+
* Managed-mode escalation for the CPA alias-restore failure (issue #4338):
|
|
373
|
+
* corrective steering recorded against one exact turn, applied by the next
|
|
374
|
+
* managed attempt that rebuilds the same turn and released on success.
|
|
375
|
+
*/
|
|
376
|
+
cpaToolAliasSteering?: AnthropicCpaToolAliasSteering;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
type AnthropicCpaToolAliasSteering = {
|
|
380
|
+
/** Corrective steering text appended to the next build of the same turn. */
|
|
381
|
+
message: string;
|
|
382
|
+
/**
|
|
383
|
+
* Fingerprint of the last user message when the failure was recorded. The
|
|
384
|
+
* steering only applies to a rebuild of the same logical turn; a later turn
|
|
385
|
+
* with a different prompt expires it instead of replaying a stale
|
|
386
|
+
* correction.
|
|
387
|
+
*/
|
|
388
|
+
turnFingerprint: string;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
392
|
+
const state: AnthropicProviderSessionState = {
|
|
393
|
+
strictToolsDisabled: false,
|
|
394
|
+
fastModeDisabled: false,
|
|
395
|
+
generatedCacheBudget: 2,
|
|
396
|
+
thinkingReplayRepairScope: "none",
|
|
397
|
+
thinkingReplayRepairAttempts: 0,
|
|
398
|
+
close: () => {
|
|
399
|
+
state.strictToolsDisabled = false;
|
|
400
|
+
state.fastModeDisabled = false;
|
|
401
|
+
state.generatedCacheBudget = 2;
|
|
402
|
+
state.thinkingReplayRepairScope = "none";
|
|
403
|
+
state.thinkingReplayRepairAttempts = 0;
|
|
404
|
+
state.thinkingReplayRejectedPayload = undefined;
|
|
405
|
+
state.cpaToolAliasSteering = undefined;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
return state;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getAnthropicProviderSessionState(
|
|
412
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
413
|
+
): AnthropicProviderSessionState | undefined {
|
|
414
|
+
if (!providerSessionState) return undefined;
|
|
415
|
+
const existing = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
|
|
416
|
+
| AnthropicProviderSessionState
|
|
417
|
+
| undefined;
|
|
418
|
+
if (existing) return existing;
|
|
419
|
+
const created = createAnthropicProviderSessionState();
|
|
420
|
+
providerSessionState.set(ANTHROPIC_PROVIDER_SESSION_STATE_KEY, created);
|
|
421
|
+
return created;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Clears the in-session "server rejected fast mode" sticky flag. Call when the
|
|
426
|
+
* caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
|
|
427
|
+
* `/fast on` after a previous turn auto-disabled it) so the next request
|
|
428
|
+
* actually carries `speed: "fast"` again. No-op when the map or state entry
|
|
429
|
+
* hasn't been materialized yet.
|
|
430
|
+
*/
|
|
431
|
+
export function clearAnthropicFastModeFallback(
|
|
432
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
433
|
+
): void {
|
|
434
|
+
if (!providerSessionState) return;
|
|
435
|
+
const state = providerSessionState.get(ANTHROPIC_PROVIDER_SESSION_STATE_KEY) as
|
|
436
|
+
| AnthropicProviderSessionState
|
|
437
|
+
| undefined;
|
|
438
|
+
if (state) state.fastModeDisabled = false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function isAnthropicStrictGrammarTooLargeError(error: unknown): boolean {
|
|
442
|
+
if (extractHttpStatusFromError(error) !== 400) return false;
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
const isStrictGrammarTooLarge = /compiled grammar/i.test(message) && /too large/i.test(message);
|
|
445
|
+
const isSchemaCompilationTooComplex =
|
|
446
|
+
/schema/i.test(message) && /too complex/i.test(message) && /compil/i.test(message);
|
|
447
|
+
return /invalid_request_error/i.test(message) && (isStrictGrammarTooLarge || isSchemaCompilationTooComplex);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function isAnthropicFastModeUnsupportedError(error: unknown): boolean {
|
|
451
|
+
const status = extractHttpStatusFromError(error);
|
|
452
|
+
if (status !== 400 && status !== 429) return false;
|
|
453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
454
|
+
// 400 invalid_request_error — model doesn't accept `speed` at all.
|
|
455
|
+
// Observed: "'Anthropic model-opus-4-5-20251101' does not support the `speed` parameter."
|
|
456
|
+
// Stay tolerant of phrasing drift ("is not supported", quoted vs backticked field).
|
|
457
|
+
if (
|
|
458
|
+
status === 400 &&
|
|
459
|
+
/invalid_request_error/i.test(message) &&
|
|
460
|
+
/\bspeed\b/i.test(message) &&
|
|
461
|
+
/not support/i.test(message)
|
|
462
|
+
) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
// 429 rate_limit_error — account lacks the extra-usage entitlement fast mode requires.
|
|
466
|
+
// Observed: "Extra usage is required for fast mode."
|
|
467
|
+
if (status === 429 && /rate_limit_error/i.test(message) && /fast mode/i.test(message)) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Proxies (e.g. CLIProxyAPI) can deliver Anthropic's 400 body as an in-stream
|
|
475
|
+
* SSE `error` event on an HTTP 200 response; the thrown error then carries no
|
|
476
|
+
* HTTP status at all (issue #3900). Accept both the direct 400 and the
|
|
477
|
+
* statusless SSE shape — the strict `invalid_request_error` message checks in
|
|
478
|
+
* each matcher keep the statusless branch from claiming unrelated failures.
|
|
479
|
+
*/
|
|
480
|
+
function isAnthropicInvalidRequestStatus(error: unknown): boolean {
|
|
481
|
+
const status = extractHttpStatusFromError(error);
|
|
482
|
+
return status === 400 || status === undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function isAnthropicThinkingBlockMutationError(error: unknown): boolean {
|
|
486
|
+
if (!isAnthropicInvalidRequestStatus(error)) return false;
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
return (
|
|
489
|
+
/invalid_request_error/i.test(message) &&
|
|
490
|
+
/thinking|redacted_thinking/i.test(message) &&
|
|
491
|
+
/latest assistant message/i.test(message) &&
|
|
492
|
+
/cannot be modified/i.test(message)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 400 shape where a replayed `thinking`/`redacted_thinking` block fails signature
|
|
498
|
+
* validation, e.g. `messages.5.content.24: Invalid \`signature\` in \`thinking\` block`.
|
|
499
|
+
* Unlike the latest-assistant mutation error above, the cited block can sit anywhere
|
|
500
|
+
* in the replayed history, so recovery must repair every assistant message rather
|
|
501
|
+
* than only the latest one.
|
|
502
|
+
*/
|
|
503
|
+
export function isAnthropicThinkingSignatureInvalidError(error: unknown): boolean {
|
|
504
|
+
if (!isAnthropicInvalidRequestStatus(error)) return false;
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
return (
|
|
507
|
+
/invalid_request_error/i.test(message) &&
|
|
508
|
+
/thinking|redacted_thinking/i.test(message) &&
|
|
509
|
+
/invalid\s+`?signature`?/i.test(message)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* CLIProxyAPI replaces Anthropic's rejection body wholesale instead of forwarding
|
|
515
|
+
* it: the client only ever sees
|
|
516
|
+
* `{"type":"error","error":{"type":"api_error","message":"An error occurred while
|
|
517
|
+
* processing the request."}}`, delivered as an in-stream SSE `error` event on an
|
|
518
|
+
* HTTP 200 response, so neither the status nor the message survives. Captured CPA
|
|
519
|
+
* traces for that masked shape carry the thinking-integrity 400 upstream (issue
|
|
520
|
+
* #3900), and the generic body matches no transient phrase either, so the turn
|
|
521
|
+
* dies unrecoverably. Nothing in the payload names the cause; callers must pair
|
|
522
|
+
* this with a request that actually replays signed thinking blocks before
|
|
523
|
+
* treating it as a thinking-replay rejection.
|
|
524
|
+
*/
|
|
525
|
+
export function isAnthropicMaskedProxyRejection(error: unknown): boolean {
|
|
526
|
+
const status = extractHttpStatusFromError(error);
|
|
527
|
+
if (status !== undefined && status !== 400) return false;
|
|
528
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
529
|
+
// A body that still names its error type is classified by the strict matchers.
|
|
530
|
+
if (/invalid_request_error/i.test(message)) return false;
|
|
531
|
+
return /"type"\s*:\s*"api_error"/.test(message) && /an error occurred while processing/i.test(message);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function fingerprintAnthropicPayload(params: MessageCreateParamsStreaming): AnthropicPayloadFingerprint {
|
|
535
|
+
const body = JSON.stringify({ ...params, stream: true });
|
|
536
|
+
return {
|
|
537
|
+
sha256: nodeCrypto.createHash("sha256").update(body).digest("hex"),
|
|
538
|
+
bytes: Buffer.byteLength(body),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function anthropicPayloadChanged(left: AnthropicPayloadFingerprint, right: AnthropicPayloadFingerprint): boolean {
|
|
543
|
+
return left.bytes !== right.bytes || left.sha256 !== right.sha256;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function extractAnthropicCitedContentPath(error: unknown): { messageIndex: number; contentIndex: number } | undefined {
|
|
547
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
548
|
+
const match = /messages\.(\d+)\.content\.(\d+)/i.exec(message);
|
|
549
|
+
if (!match) return undefined;
|
|
550
|
+
return { messageIndex: Number(match[1]), contentIndex: Number(match[2]) };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function countNativeThinkingBlocks(content: unknown): number {
|
|
554
|
+
if (!Array.isArray(content)) return 0;
|
|
555
|
+
return content.filter(block => {
|
|
556
|
+
if (!isRecord(block)) return false;
|
|
557
|
+
return block.type === "thinking" || block.type === "redacted_thinking";
|
|
558
|
+
}).length;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function describeAnthropicOutgoingPath(error: unknown, params: MessageCreateParamsStreaming): string {
|
|
562
|
+
const cited = extractAnthropicCitedContentPath(error);
|
|
563
|
+
const messages = params.messages;
|
|
564
|
+
let latestAssistantIndex = -1;
|
|
565
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
566
|
+
if (messages[index]?.role === "assistant") {
|
|
567
|
+
latestAssistantIndex = index;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const latest = latestAssistantIndex >= 0 ? messages[latestAssistantIndex] : undefined;
|
|
572
|
+
const latestThinking = countNativeThinkingBlocks(latest?.content);
|
|
573
|
+
const latestDescription =
|
|
574
|
+
latest === undefined
|
|
575
|
+
? "Vibrato's outgoing request has no assistant message"
|
|
576
|
+
: `Vibrato's latest outgoing assistant message is messages[${latestAssistantIndex}] with ${latestThinking} native thinking block(s)`;
|
|
577
|
+
if (!cited) return `${latestDescription}; the rejection did not contain a messages.N.content.M path`;
|
|
578
|
+
const outgoing = messages[cited.messageIndex];
|
|
579
|
+
if (!outgoing) {
|
|
580
|
+
return `Anthropic cited messages.${cited.messageIndex}.content.${cited.contentIndex}, but Vibrato's outgoing request has only ${messages.length} messages; ${latestDescription}`;
|
|
581
|
+
}
|
|
582
|
+
const contentBlocks = Array.isArray(outgoing.content) ? outgoing.content.length : 1;
|
|
583
|
+
return `Anthropic cited messages.${cited.messageIndex}.content.${cited.contentIndex}, but Vibrato's outgoing messages[${cited.messageIndex}] has role=${outgoing.role} and ${contentBlocks} content block(s); ${latestDescription}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function createAnthropicThinkingRepairNoopError(
|
|
587
|
+
error: unknown,
|
|
588
|
+
params: MessageCreateParamsStreaming,
|
|
589
|
+
fingerprint: AnthropicPayloadFingerprint,
|
|
590
|
+
capturedDiagnostic?: string,
|
|
591
|
+
): Error {
|
|
592
|
+
const diagnostic = describeAnthropicOutgoingPath(error, params);
|
|
593
|
+
const terminal = new Error(
|
|
594
|
+
`Anthropic thinking-replay repair was not sent because both latest-assistant and all-assistant transforms produced the same ${fingerprint.bytes}-byte payload (sha256=${fingerprint.sha256}). ${diagnostic}. Vibrato did not resend the rejected body and did not change thinking mode.${capturedDiagnostic ? `\n${capturedDiagnostic}` : ""}`,
|
|
595
|
+
);
|
|
596
|
+
const status = extractHttpStatusFromError(error);
|
|
597
|
+
if (status !== undefined) (terminal as Error & { status?: number }).status = status;
|
|
598
|
+
(terminal as Error & { anthropicHttp400AlreadyCaptured?: boolean }).anthropicHttp400AlreadyCaptured = true;
|
|
599
|
+
return terminal;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Anthropic rejects a request carrying more than four `cache_control`
|
|
604
|
+
* breakpoints. An Anthropic-compatible gateway may attach its own block-level
|
|
605
|
+
* markers before forwarding, and those never appear in the params we serialize,
|
|
606
|
+
* so no amount of local counting can predict the total. The rejection is the
|
|
607
|
+
* only evidence that our generated marker is one too many, and it is worth
|
|
608
|
+
* exactly one retry with generated caching suppressed.
|
|
609
|
+
*
|
|
610
|
+
* Our own pre-flight `validateCacheControls` failure is deliberately not
|
|
611
|
+
* matched: it carries no `invalid_request_error` wording, so a local bug stays
|
|
612
|
+
* loud instead of being silently retried.
|
|
613
|
+
*/
|
|
614
|
+
export function isAnthropicCacheBreakpointOverflowError(error: unknown): boolean {
|
|
615
|
+
if (!isAnthropicInvalidRequestStatus(error)) return false;
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
if (!/invalid_request_error/i.test(message)) return false;
|
|
618
|
+
if (!/cache_control/i.test(message)) return false;
|
|
619
|
+
// Observed: "A maximum of 4 blocks with cache_control may be provided. Found 5."
|
|
620
|
+
// Stay tolerant of phrasing drift around the limit and the reported total.
|
|
621
|
+
return /maximum of \d+ blocks/i.test(message) || /at most \d+ blocks/i.test(message);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export type AnthropicContextManagementInjectionDiagnostic = {
|
|
625
|
+
strategy: string;
|
|
626
|
+
message: string;
|
|
627
|
+
captureNote: string;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const CLEAR_THINKING_STRATEGY_PATTERN = /\b(clear_thinking_[a-z0-9_-]{1,64})\b/i;
|
|
631
|
+
|
|
632
|
+
function formatAnthropicDiagnosticBaseUrl(requestUrl: unknown): string {
|
|
633
|
+
if (typeof requestUrl !== "string") return "the configured Anthropic base URL";
|
|
634
|
+
try {
|
|
635
|
+
const url = new URL(requestUrl);
|
|
636
|
+
const basePath = url.pathname.replace(/\/v1\/messages\/?$/, "");
|
|
637
|
+
return `${url.origin}${basePath}`;
|
|
638
|
+
} catch {
|
|
639
|
+
return "the configured Anthropic base URL";
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Diagnose a context-management strategy named by an Anthropic 400 but absent
|
|
645
|
+
* from the body Vibrato sent. This mismatch is evidence of intermediary mutation,
|
|
646
|
+
* not permission to silently enable thinking or retry the request.
|
|
647
|
+
*/
|
|
648
|
+
export function diagnoseAnthropicContextManagementInjection(
|
|
649
|
+
error: unknown,
|
|
650
|
+
dump: RawHttpRequestDump | undefined,
|
|
651
|
+
): AnthropicContextManagementInjectionDiagnostic | undefined {
|
|
652
|
+
if (extractHttpStatusFromError(error) !== 400) return undefined;
|
|
653
|
+
if (dump?.api !== "anthropic-messages" || !isRecord(dump.body)) return undefined;
|
|
654
|
+
if (isAnthropicApiBaseUrl(dump.url)) return undefined;
|
|
655
|
+
if (Object.hasOwn(dump.body, "thinking") || Object.hasOwn(dump.body, "context_management")) return undefined;
|
|
656
|
+
|
|
657
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
658
|
+
if (!/invalid_request_error/i.test(errorMessage)) return undefined;
|
|
659
|
+
const strategy = CLEAR_THINKING_STRATEGY_PATTERN.exec(errorMessage)?.[1];
|
|
660
|
+
if (!strategy) return undefined;
|
|
661
|
+
if (!/\bstrategy\b/i.test(errorMessage) || !/\bthinking\b/i.test(errorMessage)) return undefined;
|
|
662
|
+
|
|
663
|
+
const baseUrl = formatAnthropicDiagnosticBaseUrl(dump.url);
|
|
664
|
+
return {
|
|
665
|
+
strategy,
|
|
666
|
+
message: [
|
|
667
|
+
`Vibrato did not send \`thinking\` or \`context_management\`, but the Anthropic 400 names the \`${strategy}\` context-management strategy.`,
|
|
668
|
+
`An intermediary at ${baseUrl} likely injected that strategy into the outgoing request.`,
|
|
669
|
+
"Enable thinking explicitly for this model, or fix/replace the intermediary so it does not add clear-thinking edits to requests without thinking. Vibrato did not auto-enable thinking or retry because that would change request cost and semantics.",
|
|
670
|
+
].join("\n"),
|
|
671
|
+
captureNote: `The HTTP 400 references context-management strategy ${strategy}, but this captured outgoing body contains neither thinking nor context_management; an intermediary may have added it after Vibrato sent the request.`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function finalizeAnthropicErrorMessage(error: unknown, dump: RawHttpRequestDump | undefined): Promise<string> {
|
|
676
|
+
if (
|
|
677
|
+
error instanceof Error &&
|
|
678
|
+
(error as Error & { anthropicHttp400AlreadyCaptured?: boolean }).anthropicHttp400AlreadyCaptured
|
|
679
|
+
) {
|
|
680
|
+
return error.message;
|
|
681
|
+
}
|
|
682
|
+
const diagnostic = diagnoseAnthropicContextManagementInjection(error, dump);
|
|
683
|
+
if (diagnostic && dump) {
|
|
684
|
+
dump.diagnostics = {
|
|
685
|
+
...(dump.diagnostics ?? {}),
|
|
686
|
+
anthropicContextManagement: {
|
|
687
|
+
strategy: diagnostic.strategy,
|
|
688
|
+
note: diagnostic.captureNote,
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const message = await finalizeErrorMessage(error, dump);
|
|
693
|
+
return diagnostic ? `${message}\n\n${diagnostic.message}` : message;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* CPA's Claude-OAuth layer cloaks downstream tool names into
|
|
698
|
+
* `mcp__<server>__<token>_<base>` aliases upstream. When the model emits a
|
|
699
|
+
* tool call whose alias embeds a token that appears nowhere in the request,
|
|
700
|
+
* CPA cannot restore the name and kills the whole stream with an HTTP 500 SSE
|
|
701
|
+
* `error` event instead of forwarding the call (issue #4338). The signature is
|
|
702
|
+
* precise and machine-parseable: it quotes the rejected alias and the failure
|
|
703
|
+
* mode verbatim. Native Anthropic never emits this phrasing, so the text itself
|
|
704
|
+
* is the route gate.
|
|
705
|
+
*/
|
|
706
|
+
const CPA_TOOL_ALIAS_RESTORE_PATTERN =
|
|
707
|
+
/cannot restore Claude OAuth MCP tool alias \\?"([^"\\\\]+)\\?": no unique request-local match/i;
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Aliases observed as `mcp__<server>__<token>_<base>` with a 12-character
|
|
711
|
+
* lowercase-alphanumeric token segment (`find` = `yw7zaf6emg3l` in both
|
|
712
|
+
* captured traces). Tolerate 8-16 chars so extraction survives token-length
|
|
713
|
+
* drift while staying out of the base name; a base that itself contains
|
|
714
|
+
* underscores (`todo_write`) is preserved by the trailing `.+`.
|
|
715
|
+
*/
|
|
716
|
+
const CPA_TOOL_ALIAS_BASE_PATTERN = /^mcp__[^_]+__[a-z0-9]{8,16}_(.+)$/;
|
|
717
|
+
|
|
718
|
+
export interface CpaToolAliasRestoreFailure {
|
|
719
|
+
/** The rejected tool-call name exactly as CPA quoted it. */
|
|
720
|
+
alias: string;
|
|
721
|
+
/**
|
|
722
|
+
* Base tool name parsed out of the alias (`mcp__<server>__<token>_<base>`
|
|
723
|
+
* → `<base>`), when the alias shape is well-formed. `undefined` for a
|
|
724
|
+
* malformed alias — callers must then fall back to direct discovery and
|
|
725
|
+
* never invent a name.
|
|
726
|
+
*/
|
|
727
|
+
baseName?: string;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Classifies the CPA alias-restore signature and extracts the rejected alias
|
|
732
|
+
* plus its base tool name. Claims only statusless in-stream SSE error events
|
|
733
|
+
* and HTTP 5xx failures: a non-5xx status carrying this text is not the
|
|
734
|
+
* observed CPA delivery shape and is left to the other classifiers.
|
|
735
|
+
*/
|
|
736
|
+
export function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined {
|
|
737
|
+
const status = extractHttpStatusFromError(error);
|
|
738
|
+
if (status !== undefined && status < 500) return undefined;
|
|
739
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
740
|
+
const match = CPA_TOOL_ALIAS_RESTORE_PATTERN.exec(message);
|
|
741
|
+
if (!match) return undefined;
|
|
742
|
+
const alias = match[1]!;
|
|
743
|
+
return { alias, baseName: CPA_TOOL_ALIAS_BASE_PATTERN.exec(alias)?.[1] };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function isCpaToolAliasRestoreFailure(error: unknown): boolean {
|
|
747
|
+
return parseCpaToolAliasRestoreFailure(error) !== undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
|
|
751
|
+
const tools = params.tools as Array<{ strict?: unknown }> | undefined;
|
|
752
|
+
return tools?.some(tool => tool.strict === true) ?? false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* `speed` lives on `BetaMessageCreateParams` (client.beta.messages) but this
|
|
757
|
+
* provider posts via `client.messages.create`, whose param type doesn't
|
|
758
|
+
* include it. This alias narrows the cast to one place.
|
|
759
|
+
*/
|
|
760
|
+
type ParamsWithSpeed = MessageCreateParamsStreaming & { speed?: "fast" };
|
|
761
|
+
|
|
762
|
+
function dropAnthropicFastMode(params: MessageCreateParamsStreaming): void {
|
|
763
|
+
delete (params as ParamsWithSpeed).speed;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function dropAnthropicStrictTools(params: MessageCreateParamsStreaming): void {
|
|
767
|
+
const tools = params.tools as Array<{ strict?: unknown }> | undefined;
|
|
768
|
+
if (!tools) return;
|
|
769
|
+
for (const tool of tools) {
|
|
770
|
+
delete tool.strict;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function isClaudeFamilyModel(model: Model<"anthropic-messages">): boolean {
|
|
775
|
+
// Classify the same identifier the request body serializes (`params.model =
|
|
776
|
+
// model.id` in buildParams); a differing `wireModelId` is not dispatched by
|
|
777
|
+
// this transport, so it must not drive the cache decision either.
|
|
778
|
+
const id = model.id;
|
|
779
|
+
const shortId = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
|
|
780
|
+
return shortId.toLowerCase().startsWith("claude-");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* How many breakpoints we are still willing to generate after a gateway has
|
|
785
|
+
* rejected a previous attempt. `explicit` mode normally emits two (a reusable
|
|
786
|
+
* prefix anchor on the last assistant turn plus a refresh point on the current
|
|
787
|
+
* user turn), so stepping down to one still caches the prefix, and only the
|
|
788
|
+
* final step gives caching up entirely.
|
|
789
|
+
*/
|
|
790
|
+
type GeneratedCacheBudget = 2 | 1 | 0;
|
|
791
|
+
|
|
792
|
+
function getCacheControl(
|
|
793
|
+
model: Model<"anthropic-messages">,
|
|
794
|
+
baseUrl: string,
|
|
795
|
+
cacheRetention?: CacheRetention,
|
|
796
|
+
generatedCacheBudget: GeneratedCacheBudget = 2,
|
|
797
|
+
): { mode: AnthropicCacheMode; cacheControl?: AnthropicCacheControl } {
|
|
798
|
+
if (generatedCacheBudget === 0) return { mode: "none" };
|
|
799
|
+
const retention = resolveCacheRetention(cacheRetention ?? model.cacheRetention, "long");
|
|
800
|
+
if (retention === "none") return { mode: "none" };
|
|
801
|
+
|
|
802
|
+
const isCanonicalApi = isAnthropicApiBaseUrl(baseUrl);
|
|
803
|
+
const promptCacheMode = model.compat?.promptCacheMode;
|
|
804
|
+
const mode: AnthropicCacheMode =
|
|
805
|
+
promptCacheMode === "none"
|
|
806
|
+
? "none"
|
|
807
|
+
: promptCacheMode === "explicit"
|
|
808
|
+
? "explicit"
|
|
809
|
+
: promptCacheMode === "automatic"
|
|
810
|
+
? "automatic"
|
|
811
|
+
: isCanonicalApi
|
|
812
|
+
? "automatic"
|
|
813
|
+
: isClaudeFamilyModel(model)
|
|
814
|
+
? "explicit"
|
|
815
|
+
: "none";
|
|
816
|
+
if (mode === "none") return { mode };
|
|
817
|
+
|
|
818
|
+
const supportsLongCacheRetention = isCanonicalApi
|
|
819
|
+
? getAnthropicCompat(model).supportsLongCacheRetention
|
|
820
|
+
: model.compat?.supportsLongCacheRetention === true;
|
|
821
|
+
return {
|
|
822
|
+
mode,
|
|
823
|
+
cacheControl: {
|
|
824
|
+
type: "ephemeral",
|
|
825
|
+
...(retention === "long" && supportsLongCacheRetention ? { ttl: "1h" } : {}),
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Stealth mode: Mimic Anthropic Code headers and tool prefixing.
|
|
831
|
+
export const claudeCodeVersion = "2.1.219";
|
|
832
|
+
export const claudeCodeEntrypoint = "sdk-cli";
|
|
833
|
+
export const claudeToolPrefix: string = "proxy_";
|
|
834
|
+
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
835
|
+
|
|
836
|
+
export function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
|
|
837
|
+
switch (platform.toLowerCase()) {
|
|
838
|
+
case "darwin":
|
|
839
|
+
return "MacOS";
|
|
840
|
+
case "windows":
|
|
841
|
+
case "win32":
|
|
842
|
+
return "Windows";
|
|
843
|
+
case "linux":
|
|
844
|
+
return "Linux";
|
|
845
|
+
case "freebsd":
|
|
846
|
+
return "FreeBSD";
|
|
847
|
+
default:
|
|
848
|
+
return `Other::${platform.toLowerCase()}`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
|
|
853
|
+
switch (arch.toLowerCase()) {
|
|
854
|
+
case "amd64":
|
|
855
|
+
case "x64":
|
|
856
|
+
return "x64";
|
|
857
|
+
case "arm64":
|
|
858
|
+
case "aarch64":
|
|
859
|
+
return "arm64";
|
|
860
|
+
case "386":
|
|
861
|
+
case "x86":
|
|
862
|
+
case "ia32":
|
|
863
|
+
return "x86";
|
|
864
|
+
default:
|
|
865
|
+
return `other::${arch.toLowerCase()}`;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export const claudeCodeHeaders = {
|
|
870
|
+
"X-Stainless-Retry-Count": "0",
|
|
871
|
+
"X-Stainless-Runtime-Version": "v24.3.0",
|
|
872
|
+
"X-Stainless-Package-Version": "0.74.0",
|
|
873
|
+
"X-Stainless-Runtime": "node",
|
|
874
|
+
"X-Stainless-Lang": "js",
|
|
875
|
+
"X-Stainless-Arch": mapStainlessArch(process.arch),
|
|
876
|
+
"X-Stainless-Os": mapStainlessOs(process.platform),
|
|
877
|
+
"X-Stainless-Timeout": "600",
|
|
878
|
+
} as const;
|
|
879
|
+
|
|
880
|
+
const enforcedHeaderKeys = new Set(
|
|
881
|
+
[
|
|
882
|
+
...Object.keys(claudeCodeHeaders),
|
|
883
|
+
"Accept",
|
|
884
|
+
"Accept-Encoding",
|
|
885
|
+
"Connection",
|
|
886
|
+
"Content-Type",
|
|
887
|
+
"Anthropic-Version",
|
|
888
|
+
"Anthropic-Dangerous-Direct-Browser-Access",
|
|
889
|
+
"Anthropic-Beta",
|
|
890
|
+
"User-Agent",
|
|
891
|
+
"X-App",
|
|
892
|
+
"Authorization",
|
|
893
|
+
"X-Api-Key",
|
|
894
|
+
"cf-aig-authorization",
|
|
895
|
+
].map(key => key.toLowerCase()),
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
899
|
+
|
|
900
|
+
function createClaudeBillingHeader(payload: unknown): string {
|
|
901
|
+
const payloadJson = JSON.stringify(payload) ?? "";
|
|
902
|
+
const cch = nodeCrypto.createHash("sha256").update(payloadJson).digest("hex").slice(0, 5);
|
|
903
|
+
const randomBytes = new Uint8Array(2);
|
|
904
|
+
crypto.getRandomValues(randomBytes);
|
|
905
|
+
const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0"))
|
|
906
|
+
.join("")
|
|
907
|
+
.slice(0, 3);
|
|
908
|
+
return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${claudeCodeVersion}.${buildHash}; cc_entrypoint=${claudeCodeEntrypoint}; cch=${cch};`;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
912
|
+
/^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}$/;
|
|
913
|
+
|
|
914
|
+
export function isClaudeCloakingUserId(userId: string): boolean {
|
|
915
|
+
return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Real Anthropic Code sends `metadata.user_id` as a JSON-stringified object of the
|
|
920
|
+
* shape `{ device_id, account_uuid, session_id, ...extra }` (see
|
|
921
|
+
* services/api/Anthropic model.ts → getAPIMetadata). Accept that shape so callers that
|
|
922
|
+
* supply a stable `session_id` aren't silently overwritten with fresh entropy
|
|
923
|
+
* on every request, which would inflate the backend session count.
|
|
924
|
+
*/
|
|
925
|
+
function isClaudeJsonUserId(userId: string): boolean {
|
|
926
|
+
if (userId.length === 0 || userId[0] !== "{") return false;
|
|
927
|
+
let parsed: unknown;
|
|
928
|
+
try {
|
|
929
|
+
parsed = JSON.parse(userId);
|
|
930
|
+
} catch {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
934
|
+
const obj = parsed as Record<string, unknown>;
|
|
935
|
+
return typeof obj.session_id === "string" && obj.session_id.length > 0;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export function generateClaudeCloakingUserId(): string {
|
|
939
|
+
const userHash = nodeCrypto.randomBytes(32).toString("hex");
|
|
940
|
+
const accountId = nodeCrypto.randomUUID().toLowerCase();
|
|
941
|
+
const sessionId = nodeCrypto.randomUUID().toLowerCase();
|
|
942
|
+
return `user_${userHash}_account_${accountId}_session_${sessionId}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function resolveAnthropicMetadataUserId(userId: unknown, isOAuthToken: boolean): string | undefined {
|
|
946
|
+
if (typeof userId === "string") {
|
|
947
|
+
if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
|
|
948
|
+
return userId;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!isOAuthToken) return undefined;
|
|
953
|
+
return generateClaudeCloakingUserId();
|
|
954
|
+
}
|
|
955
|
+
const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
|
|
956
|
+
export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
|
|
957
|
+
if (!prefixOverride) return name;
|
|
958
|
+
if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
|
|
959
|
+
return `${prefixOverride}${name}`;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
|
|
963
|
+
if (!prefixOverride) return name;
|
|
964
|
+
if (!name.startsWith(prefixOverride)) return name;
|
|
965
|
+
return name.slice(prefixOverride.length);
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Anthropic requires image `data` to be standard (RFC 4648) base64: the standard
|
|
969
|
+
// alphabet only, correct quartet grouping, and padding (when present) confined to
|
|
970
|
+
// a trailing `=`/`==`. A resident image whose blob went missing bakes a
|
|
971
|
+
// human-readable placeholder into `data` (e.g. "[Session resident imageData blob
|
|
972
|
+
// missing: …]"), and other callers can pass whitespace, data URLs, or URL-safe
|
|
973
|
+
// variants — all of which the API rejects with a 400 `invalid base64 data` that
|
|
974
|
+
// fails the *entire* request and bricks the session. Validate the wire format
|
|
975
|
+
// strictly and degrade anything that is not standard base64 to text.
|
|
976
|
+
//
|
|
977
|
+
// Accepts canonical padded forms and their unpadded equivalents; rejects
|
|
978
|
+
// length % 4 === 1, misplaced/overlong padding, whitespace, data URLs, URL-safe
|
|
979
|
+
// (`-`/`_`) alphabets, prose, and empty input. The pattern has no nested
|
|
980
|
+
// quantifier, so even oversized inputs are rejected in linear time.
|
|
981
|
+
const ANTHROPIC_BASE64_IMAGE_DATA = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
|
|
982
|
+
function isAnthropicBase64ImageData(data: string): boolean {
|
|
983
|
+
return data.length > 0 && data.length % 4 !== 1 && ANTHROPIC_BASE64_IMAGE_DATA.test(data);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Convert content blocks to Anthropic API format
|
|
988
|
+
*/
|
|
989
|
+
function convertContentBlocks(
|
|
990
|
+
content: (TextContent | ImageContent)[],
|
|
991
|
+
supportsImages = true,
|
|
992
|
+
):
|
|
993
|
+
| string
|
|
994
|
+
| Array<
|
|
995
|
+
| { type: "text"; text: string }
|
|
996
|
+
| {
|
|
997
|
+
type: "image";
|
|
998
|
+
source: {
|
|
999
|
+
type: "base64";
|
|
1000
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
1001
|
+
data: string;
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
> {
|
|
1005
|
+
const textBlocks = content
|
|
1006
|
+
.filter((block): block is TextContent => block.type === "text")
|
|
1007
|
+
.map(block => block.text.toWellFormed())
|
|
1008
|
+
.filter(text => text.trim().length > 0);
|
|
1009
|
+
const imageBlocks: ImageContent[] = [];
|
|
1010
|
+
for (const block of content) {
|
|
1011
|
+
if (block.type !== "image") continue;
|
|
1012
|
+
if (isAnthropicBase64ImageData(block.data)) {
|
|
1013
|
+
imageBlocks.push(block);
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
// Non-base64 image payload (e.g. a missing-blob placeholder): degrade to
|
|
1017
|
+
// text so one lost image cannot invalidate the entire request.
|
|
1018
|
+
const text = block.data.toWellFormed().trim();
|
|
1019
|
+
if (text.length > 0) textBlocks.push(text);
|
|
1020
|
+
}
|
|
1021
|
+
const omittedImages = !supportsImages && imageBlocks.length > 0;
|
|
1022
|
+
if (imageBlocks.length === 0 || !supportsImages) {
|
|
1023
|
+
if (omittedImages) {
|
|
1024
|
+
textBlocks.push(NON_VISION_IMAGE_PLACEHOLDER);
|
|
1025
|
+
}
|
|
1026
|
+
return textBlocks.join("\n").toWellFormed();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const blocks = [
|
|
1030
|
+
...textBlocks.map(text => ({
|
|
1031
|
+
type: "text" as const,
|
|
1032
|
+
text,
|
|
1033
|
+
})),
|
|
1034
|
+
...imageBlocks.map(block => ({
|
|
1035
|
+
type: "image" as const,
|
|
1036
|
+
source: {
|
|
1037
|
+
type: "base64" as const,
|
|
1038
|
+
media_type: block.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
1039
|
+
data: block.data,
|
|
1040
|
+
},
|
|
1041
|
+
})),
|
|
1042
|
+
];
|
|
1043
|
+
|
|
1044
|
+
if (!textBlocks.length) {
|
|
1045
|
+
blocks.unshift({
|
|
1046
|
+
type: "text" as const,
|
|
1047
|
+
text: "(see attached image)",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return blocks;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
1055
|
+
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
1056
|
+
|
|
1057
|
+
export interface AnthropicOptions extends StreamOptions {
|
|
1058
|
+
/**
|
|
1059
|
+
* Enable extended thinking.
|
|
1060
|
+
* For Opus 4.6+: uses adaptive thinking (Anthropic model decides when/how much to think).
|
|
1061
|
+
* For older models: uses budget-based thinking with thinkingBudgetTokens.
|
|
1062
|
+
*/
|
|
1063
|
+
thinkingEnabled?: boolean;
|
|
1064
|
+
/**
|
|
1065
|
+
* Token budget for extended thinking (older models only).
|
|
1066
|
+
* Ignored for Opus 4.6+ which uses adaptive thinking.
|
|
1067
|
+
*/
|
|
1068
|
+
thinkingBudgetTokens?: number;
|
|
1069
|
+
/**
|
|
1070
|
+
* Effort level for adaptive thinking (Opus 4.6+ only).
|
|
1071
|
+
* Controls how much thinking Anthropic model allocates:
|
|
1072
|
+
* - "max": Always thinks with no constraints
|
|
1073
|
+
* - "high": Always thinks, deep reasoning (default)
|
|
1074
|
+
* - "medium": Moderate thinking, may skip for simple queries
|
|
1075
|
+
* - "low": Minimal thinking, skips for simple tasks
|
|
1076
|
+
* Ignored for older models.
|
|
1077
|
+
*/
|
|
1078
|
+
effort?: AnthropicEffort;
|
|
1079
|
+
/**
|
|
1080
|
+
* Optional reasoning level fallback for direct Anthropic provider usage.
|
|
1081
|
+
* Converted to adaptive effort when effort is not explicitly provided.
|
|
1082
|
+
*/
|
|
1083
|
+
reasoning?: SimpleStreamOptions["reasoning"];
|
|
1084
|
+
/**
|
|
1085
|
+
* Controls how Anthropic returns thinking content when the selected thinking
|
|
1086
|
+
* transport supports a display option. Defaults to "summarized" where the
|
|
1087
|
+
* API accepts it.
|
|
1088
|
+
*/
|
|
1089
|
+
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
1090
|
+
interleavedThinking?: boolean;
|
|
1091
|
+
toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string };
|
|
1092
|
+
betas?: string[] | string;
|
|
1093
|
+
/**
|
|
1094
|
+
* Realization of `serviceTier: "priority"` on Anthropic models. When
|
|
1095
|
+
* `"priority"`, sets `speed: "fast"` on the request and appends the
|
|
1096
|
+
* `fast-mode-2026-02-01` beta header. Anthropic rejects unsupported models
|
|
1097
|
+
* with `invalid_request_error`, which triggers an in-provider one-shot
|
|
1098
|
+
* fallback (see `fastModeDisabled` provider state).
|
|
1099
|
+
*
|
|
1100
|
+
* Other `ServiceTier` values are currently ignored on this provider.
|
|
1101
|
+
*/
|
|
1102
|
+
serviceTier?: ServiceTier;
|
|
1103
|
+
/** Force OAuth bearer auth mode for proxy tokens that don't match Anthropic token prefixes. */
|
|
1104
|
+
isOAuth?: boolean;
|
|
1105
|
+
/**
|
|
1106
|
+
* Pre-built Anthropic client instance. When provided, skips internal client
|
|
1107
|
+
* construction entirely. Use this to inject alternative SDK clients such as
|
|
1108
|
+
* `AnthropicVertex` that shares the same messaging API.
|
|
1109
|
+
*/
|
|
1110
|
+
client?: Anthropic;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export type AnthropicClientOptionsArgs = {
|
|
1114
|
+
model: Model<"anthropic-messages">;
|
|
1115
|
+
apiKey: string;
|
|
1116
|
+
extraBetas?: string[];
|
|
1117
|
+
stream?: boolean;
|
|
1118
|
+
interleavedThinking?: boolean;
|
|
1119
|
+
headers?: Record<string, string>;
|
|
1120
|
+
dynamicHeaders?: Record<string, string>;
|
|
1121
|
+
isOAuth?: boolean;
|
|
1122
|
+
hasTools?: boolean;
|
|
1123
|
+
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
1124
|
+
fetch?: FetchImpl;
|
|
1125
|
+
requestMaxRetries?: number;
|
|
1126
|
+
maxRetryDelayMs?: number;
|
|
1127
|
+
streamFirstEventTimeoutMs?: number;
|
|
1128
|
+
streamIdleTimeoutMs?: number;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
export type AnthropicClientOptionsResult = {
|
|
1132
|
+
isOAuthToken: boolean;
|
|
1133
|
+
apiKey: string | null;
|
|
1134
|
+
authToken?: string | null;
|
|
1135
|
+
baseURL?: string;
|
|
1136
|
+
maxRetries: number;
|
|
1137
|
+
timeout?: number;
|
|
1138
|
+
dangerouslyAllowBrowser: boolean;
|
|
1139
|
+
defaultHeaders: Record<string, string>;
|
|
1140
|
+
logLevel: AnthropicSdkClientOptions["logLevel"];
|
|
1141
|
+
fetch?: AnthropicSdkClientOptions["fetch"];
|
|
1142
|
+
fetchOptions?: AnthropicSdkClientOptions["fetchOptions"];
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const CLAUDE_CODE_TLS_CIPHERS = tls.DEFAULT_CIPHERS;
|
|
1146
|
+
|
|
1147
|
+
type FoundryTlsOptions = {
|
|
1148
|
+
ca?: string | string[];
|
|
1149
|
+
cert?: string;
|
|
1150
|
+
key?: string;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
export function resolveGlmZcodeAnthropicBaseUrl(): string {
|
|
1154
|
+
const configured = $credentialEnv("ZCODE_PLAN_ANTHROPIC_BASE_URL")?.trim();
|
|
1155
|
+
if (!configured || /[\u0000-\u001f\u007f-\u009f]/u.test(configured)) {
|
|
1156
|
+
return GLM_ZCODE_ANTHROPIC_BASE_URL;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = new URL(configured);
|
|
1160
|
+
if (
|
|
1161
|
+
parsed.protocol !== "https:" ||
|
|
1162
|
+
parsed.hostname.length === 0 ||
|
|
1163
|
+
parsed.username.length > 0 ||
|
|
1164
|
+
parsed.password.length > 0 ||
|
|
1165
|
+
parsed.search.length > 0 ||
|
|
1166
|
+
parsed.hash.length > 0
|
|
1167
|
+
) {
|
|
1168
|
+
return GLM_ZCODE_ANTHROPIC_BASE_URL;
|
|
1169
|
+
}
|
|
1170
|
+
return normalizeAnthropicBaseUrl(parsed.toString()) ?? GLM_ZCODE_ANTHROPIC_BASE_URL;
|
|
1171
|
+
} catch {
|
|
1172
|
+
return GLM_ZCODE_ANTHROPIC_BASE_URL;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: string): string | undefined {
|
|
1177
|
+
if (model.provider === "github-copilot") {
|
|
1178
|
+
return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
|
|
1179
|
+
}
|
|
1180
|
+
// glm-zcode logs in via ZCode's OAuth but auto-provisions a real Z.AI API key and
|
|
1181
|
+
// calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
|
|
1182
|
+
// discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
|
|
1183
|
+
if (model.provider === "glm-zcode") {
|
|
1184
|
+
return resolveGlmZcodeAnthropicBaseUrl();
|
|
1185
|
+
}
|
|
1186
|
+
if (model.provider === "anthropic" && isFoundryEnabled()) {
|
|
1187
|
+
const foundryBaseUrl = normalizeAnthropicBaseUrl($credentialEnv("FOUNDRY_BASE_URL"));
|
|
1188
|
+
if (foundryBaseUrl) {
|
|
1189
|
+
return foundryBaseUrl;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (model.provider === "anthropic") {
|
|
1193
|
+
return normalizeAnthropicBaseUrl(model.baseUrl) ?? "https://api.anthropic.com";
|
|
1194
|
+
}
|
|
1195
|
+
return normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
|
|
1199
|
+
const source = rawHeaders?.trim();
|
|
1200
|
+
if (!source) return undefined;
|
|
1201
|
+
|
|
1202
|
+
const parsed: Record<string, string> = {};
|
|
1203
|
+
for (const token of source.split(/\r?\n|,/)) {
|
|
1204
|
+
const entry = token.trim();
|
|
1205
|
+
if (!entry) continue;
|
|
1206
|
+
const separatorIndex = entry.indexOf(":");
|
|
1207
|
+
if (separatorIndex <= 0) continue;
|
|
1208
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
1209
|
+
const value = entry.slice(separatorIndex + 1).trim();
|
|
1210
|
+
if (!key || !value) continue;
|
|
1211
|
+
parsed[key] = value;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return Object.keys(parsed).length > 0 ? parsed : undefined;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function resolveAnthropicCustomHeaders(model: Model<"anthropic-messages">): Record<string, string> | undefined {
|
|
1218
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1219
|
+
if (!isFoundryEnabled()) return undefined;
|
|
1220
|
+
return parseAnthropicCustomHeaders($env.ANTHROPIC_CUSTOM_HEADERS);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function looksLikeFilePath(value: string): boolean {
|
|
1224
|
+
return value.includes("/") || value.includes("\\") || /\.(pem|crt|cer|key)$/i.test(value);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function resolvePemValue(value: string | undefined, name: string): string | undefined {
|
|
1228
|
+
const trimmed = value?.trim();
|
|
1229
|
+
if (!trimmed) return undefined;
|
|
1230
|
+
|
|
1231
|
+
const inline = trimmed.replace(/\\n/g, "\n");
|
|
1232
|
+
if (inline.includes("-----BEGIN")) {
|
|
1233
|
+
return inline;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (looksLikeFilePath(trimmed)) {
|
|
1237
|
+
try {
|
|
1238
|
+
return fs.readFileSync(trimmed, "utf8");
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
if (isEnoent(error)) {
|
|
1241
|
+
throw new Error(`${name} path does not exist: ${trimmed}`);
|
|
1242
|
+
}
|
|
1243
|
+
throw error;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return inline;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTlsOptions | undefined {
|
|
1251
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1252
|
+
if (!isFoundryEnabled()) return undefined;
|
|
1253
|
+
|
|
1254
|
+
const ca = resolvePemValue($env.NODE_EXTRA_CA_CERTS, "NODE_EXTRA_CA_CERTS");
|
|
1255
|
+
const cert = resolvePemValue($env.CLAUDE_CODE_CLIENT_CERT, "CLAUDE_CODE_CLIENT_CERT");
|
|
1256
|
+
const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
|
|
1257
|
+
|
|
1258
|
+
if ((cert && !key) || (!cert && key)) {
|
|
1259
|
+
throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const options: FoundryTlsOptions = {};
|
|
1263
|
+
if (ca) options.ca = [...tls.rootCertificates, ca];
|
|
1264
|
+
if (cert) options.cert = cert;
|
|
1265
|
+
if (key) options.key = key;
|
|
1266
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function buildClaudeCodeTlsFetchOptions(
|
|
1270
|
+
model: Model<"anthropic-messages">,
|
|
1271
|
+
baseUrl: string | undefined,
|
|
1272
|
+
): AnthropicSdkClientOptions["fetchOptions"] | undefined {
|
|
1273
|
+
if (model.provider !== "anthropic") return undefined;
|
|
1274
|
+
if (!baseUrl) return undefined;
|
|
1275
|
+
|
|
1276
|
+
let serverName: string;
|
|
1277
|
+
try {
|
|
1278
|
+
serverName = new URL(baseUrl).hostname;
|
|
1279
|
+
} catch {
|
|
1280
|
+
return undefined;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (!serverName) return undefined;
|
|
1284
|
+
|
|
1285
|
+
const foundryTlsOptions = resolveFoundryTlsOptions(model);
|
|
1286
|
+
|
|
1287
|
+
return {
|
|
1288
|
+
tls: {
|
|
1289
|
+
rejectUnauthorized: true,
|
|
1290
|
+
serverName,
|
|
1291
|
+
...(CLAUDE_CODE_TLS_CIPHERS ? { ciphers: CLAUDE_CODE_TLS_CIPHERS } : {}),
|
|
1292
|
+
...(foundryTlsOptions ?? {}),
|
|
1293
|
+
},
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string> {
|
|
1297
|
+
const merged: Record<string, string> = {};
|
|
1298
|
+
for (const headers of headerSources) {
|
|
1299
|
+
if (headers) {
|
|
1300
|
+
Object.assign(merged, headers);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return merged;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const ANTHROPIC_RETRY_DELAY_CAP_MS = 60_000;
|
|
1307
|
+
const ANTHROPIC_RATE_LIMIT_HEADER_PREFIX = "anthropic-ratelimit-";
|
|
1308
|
+
|
|
1309
|
+
function getSafeAnthropicHeaderEvidence(headers: Headers): string[] {
|
|
1310
|
+
const evidence: string[] = [];
|
|
1311
|
+
for (const [name, value] of headers) {
|
|
1312
|
+
const lowerName = name.toLowerCase();
|
|
1313
|
+
if (
|
|
1314
|
+
lowerName === "retry-after" ||
|
|
1315
|
+
lowerName === "retry-after-ms" ||
|
|
1316
|
+
lowerName.startsWith(ANTHROPIC_RATE_LIMIT_HEADER_PREFIX)
|
|
1317
|
+
) {
|
|
1318
|
+
evidence.push(`${lowerName}=${value}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return evidence.sort();
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function getStringProperty(source: unknown, key: string): string | undefined {
|
|
1325
|
+
if (!isRecord(source)) return undefined;
|
|
1326
|
+
const value = source[key];
|
|
1327
|
+
return typeof value === "string" ? value : undefined;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function appendAnthropicRateLimitEvidence(bodyText: string, headers: Headers): string {
|
|
1331
|
+
const evidence = getSafeAnthropicHeaderEvidence(headers);
|
|
1332
|
+
if (evidence.length === 0) return bodyText;
|
|
1333
|
+
|
|
1334
|
+
const suffix = ` Anthropic rate-limit evidence: ${evidence.join(", ")}`;
|
|
1335
|
+
try {
|
|
1336
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
1337
|
+
if (isRecord(parsed)) {
|
|
1338
|
+
const error = parsed.error;
|
|
1339
|
+
if (isRecord(error) && typeof error.message === "string" && !error.message.includes(suffix)) {
|
|
1340
|
+
return JSON.stringify({ ...parsed, error: { ...error, message: `${error.message}${suffix}` } });
|
|
1341
|
+
}
|
|
1342
|
+
if (typeof parsed.message === "string" && !parsed.message.includes(suffix)) {
|
|
1343
|
+
return JSON.stringify({ ...parsed, message: `${parsed.message}${suffix}` });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch {}
|
|
1347
|
+
|
|
1348
|
+
return bodyText.includes(suffix) ? bodyText : `${bodyText}${suffix}`;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function isAnthropicUsageExhaustionResponse(
|
|
1352
|
+
bodyText: string,
|
|
1353
|
+
headers: Headers,
|
|
1354
|
+
retryAfterMs: number | undefined,
|
|
1355
|
+
retryDelayCapMs: number,
|
|
1356
|
+
): boolean {
|
|
1357
|
+
const overageReason = headers.get("anthropic-ratelimit-unified-overage-disabled-reason")?.toLowerCase();
|
|
1358
|
+
if (overageReason === "out_of_credits") return true;
|
|
1359
|
+
if (retryAfterMs !== undefined && retryAfterMs > retryDelayCapMs) return true;
|
|
1360
|
+
|
|
1361
|
+
try {
|
|
1362
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
1363
|
+
const error = isRecord(parsed) ? parsed.error : undefined;
|
|
1364
|
+
const type = getStringProperty(error, "type") ?? getStringProperty(parsed, "type");
|
|
1365
|
+
const message = getStringProperty(error, "message") ?? getStringProperty(parsed, "message") ?? bodyText;
|
|
1366
|
+
return (
|
|
1367
|
+
/rate_limit_error/i.test(type ?? "") &&
|
|
1368
|
+
(/request would exceed your account.?s rate limit/i.test(message) || /out_of_credits/i.test(message))
|
|
1369
|
+
);
|
|
1370
|
+
} catch {
|
|
1371
|
+
return /request would exceed your account.?s rate limit|out_of_credits/i.test(bodyText);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function wrapAnthropicFetchForBoundedRateLimits(baseFetch: FetchImpl, maxRetryDelayMs: number | undefined): FetchImpl {
|
|
1376
|
+
const retryDelayCapMs = maxRetryDelayMs ?? ANTHROPIC_RETRY_DELAY_CAP_MS;
|
|
1377
|
+
return Object.assign(
|
|
1378
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
1379
|
+
const response = await baseFetch(input, init);
|
|
1380
|
+
if (response.status !== 429 || retryDelayCapMs === 0) return response;
|
|
1381
|
+
|
|
1382
|
+
const headers = new Headers(response.headers);
|
|
1383
|
+
const retryAfterMs = getRetryAfterMsFromHeaders(headers);
|
|
1384
|
+
const bodyText = await response
|
|
1385
|
+
.clone()
|
|
1386
|
+
.text()
|
|
1387
|
+
.catch(() => "");
|
|
1388
|
+
if (!isAnthropicUsageExhaustionResponse(bodyText, headers, retryAfterMs, retryDelayCapMs)) return response;
|
|
1389
|
+
|
|
1390
|
+
headers.set("x-should-retry", "false");
|
|
1391
|
+
return new Response(appendAnthropicRateLimitEvidence(bodyText, headers), {
|
|
1392
|
+
status: response.status,
|
|
1393
|
+
statusText: response.statusText,
|
|
1394
|
+
headers,
|
|
1395
|
+
});
|
|
1396
|
+
},
|
|
1397
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
|
|
1402
|
+
// We surface the resulting provider error ourselves, so keep the SDK quiet.
|
|
1403
|
+
const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
|
|
1404
|
+
|
|
1405
|
+
const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
|
|
1406
|
+
"message_start",
|
|
1407
|
+
"message_delta",
|
|
1408
|
+
"message_stop",
|
|
1409
|
+
"content_block_start",
|
|
1410
|
+
"content_block_delta",
|
|
1411
|
+
"content_block_stop",
|
|
1412
|
+
]);
|
|
1413
|
+
|
|
1414
|
+
async function* iterateAnthropicEvents(
|
|
1415
|
+
response: Response,
|
|
1416
|
+
signal?: AbortSignal,
|
|
1417
|
+
onSseEvent?: AnthropicOptions["onSseEvent"],
|
|
1418
|
+
): AsyncGenerator<RawMessageStreamEvent> {
|
|
1419
|
+
if (!response.body) {
|
|
1420
|
+
throw new Error("Attempted to iterate over an Anthropic response with no body");
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
let sawMessageStart = false;
|
|
1424
|
+
let sawMessageEnd = false;
|
|
1425
|
+
|
|
1426
|
+
for await (const sse of readSseEvents(response.body, signal)) {
|
|
1427
|
+
notifyRawSseEvent(onSseEvent, sse);
|
|
1428
|
+
if (sse.event === "error") {
|
|
1429
|
+
throw new Error(sse.data);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
try {
|
|
1437
|
+
const event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);
|
|
1438
|
+
if (event.type === "message_start") {
|
|
1439
|
+
sawMessageStart = true;
|
|
1440
|
+
} else if (event.type === "message_stop") {
|
|
1441
|
+
sawMessageEnd = true;
|
|
1442
|
+
}
|
|
1443
|
+
yield event;
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
`Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join("\\n")}`,
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (sawMessageStart && !sawMessageEnd) {
|
|
1453
|
+
throw createAnthropicStreamEnvelopeError("stream ended before message_stop");
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
type AnthropicRawResponseRequest = {
|
|
1458
|
+
asResponse(): Promise<Response>;
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
function hasAnthropicRawResponseRequest(request: unknown): request is AnthropicRawResponseRequest {
|
|
1462
|
+
return isRecord(request) && typeof request.asResponse === "function";
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
type AnthropicStreamWithResponseRequest = {
|
|
1466
|
+
withResponse(): Promise<{
|
|
1467
|
+
data: AsyncIterable<RawMessageStreamEvent>;
|
|
1468
|
+
response: Response;
|
|
1469
|
+
request_id: string | null;
|
|
1470
|
+
}>;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
function hasAnthropicStreamWithResponseRequest(request: unknown): request is AnthropicStreamWithResponseRequest {
|
|
1474
|
+
return isRecord(request) && typeof request.withResponse === "function";
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function getAnthropicStreamResponse(
|
|
1478
|
+
request: unknown,
|
|
1479
|
+
signal?: AbortSignal,
|
|
1480
|
+
onSseEvent?: AnthropicOptions["onSseEvent"],
|
|
1481
|
+
): Promise<{ events: AsyncIterable<RawMessageStreamEvent>; response: Response; requestId: string | null }> {
|
|
1482
|
+
if (hasAnthropicRawResponseRequest(request)) {
|
|
1483
|
+
const response = await request.asResponse();
|
|
1484
|
+
return {
|
|
1485
|
+
events: iterateAnthropicEvents(response, signal, onSseEvent),
|
|
1486
|
+
response,
|
|
1487
|
+
requestId: response.headers.get("request-id"),
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
if (hasAnthropicStreamWithResponseRequest(request)) {
|
|
1491
|
+
const { data, response, request_id } = await request.withResponse();
|
|
1492
|
+
return { events: data, response, requestId: request_id };
|
|
1493
|
+
}
|
|
1494
|
+
throw new Error("Anthropic SDK request did not expose a stream response");
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function getAnthropicCompat(
|
|
1498
|
+
model: Model<"anthropic-messages">,
|
|
1499
|
+
): Required<Omit<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport">> &
|
|
1500
|
+
Pick<NonNullable<Model<"anthropic-messages">["compat"]>, "toolChoiceSupport"> {
|
|
1501
|
+
return {
|
|
1502
|
+
disableStrictTools: model.compat?.disableStrictTools ?? false,
|
|
1503
|
+
disableAdaptiveThinking: model.compat?.disableAdaptiveThinking ?? false,
|
|
1504
|
+
supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
|
|
1505
|
+
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
|
|
1506
|
+
supportsToolChoice: model.compat?.supportsToolChoice ?? true,
|
|
1507
|
+
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
|
|
1508
|
+
promptCacheMode: model.compat?.promptCacheMode ?? "none",
|
|
1509
|
+
toolChoiceSupport: model.compat?.toolChoiceSupport,
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const PROVIDER_MAX_RETRIES = 3;
|
|
1514
|
+
const PROVIDER_BASE_DELAY_MS = 2000;
|
|
1515
|
+
const ANTHROPIC_CUSTOM_ENDPOINT_FIRST_EVENT_GRACE_MS = 120_000;
|
|
1516
|
+
const ANTHROPIC_LARGE_REQUEST_BYTES = 1_000_000;
|
|
1517
|
+
const ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS = 1;
|
|
1518
|
+
const ANTHROPIC_SMALL_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS = 2;
|
|
1519
|
+
|
|
1520
|
+
function classifyAnthropicEndpoint(baseUrl: string): "canonical" | "custom" {
|
|
1521
|
+
try {
|
|
1522
|
+
const url = new URL(baseUrl);
|
|
1523
|
+
return url.protocol.toLowerCase() === "https:" &&
|
|
1524
|
+
url.hostname.toLowerCase() === "api.anthropic.com" &&
|
|
1525
|
+
(url.port === "" || url.port === "443") &&
|
|
1526
|
+
url.username === "" &&
|
|
1527
|
+
url.password === "" &&
|
|
1528
|
+
url.search === "" &&
|
|
1529
|
+
url.hash === "" &&
|
|
1530
|
+
(url.pathname === "" || url.pathname === "/")
|
|
1531
|
+
? "canonical"
|
|
1532
|
+
: "custom";
|
|
1533
|
+
} catch {
|
|
1534
|
+
return "custom";
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function resolveAnthropicFirstEventWatchdogMs(
|
|
1539
|
+
firstEventTimeoutMs: number | undefined,
|
|
1540
|
+
endpointClass: "canonical" | "custom",
|
|
1541
|
+
requestBytes: number,
|
|
1542
|
+
): number | undefined {
|
|
1543
|
+
if (
|
|
1544
|
+
firstEventTimeoutMs === undefined ||
|
|
1545
|
+
firstEventTimeoutMs <= 0 ||
|
|
1546
|
+
endpointClass === "canonical" ||
|
|
1547
|
+
requestBytes < ANTHROPIC_LARGE_REQUEST_BYTES
|
|
1548
|
+
) {
|
|
1549
|
+
return firstEventTimeoutMs;
|
|
1550
|
+
}
|
|
1551
|
+
return firstEventTimeoutMs + ANTHROPIC_CUSTOM_ENDPOINT_FIRST_EVENT_GRACE_MS;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function resolveAnthropicFirstEventTimeoutMaxAttempts(requestBytes: number): number {
|
|
1555
|
+
return requestBytes >= ANTHROPIC_LARGE_REQUEST_BYTES
|
|
1556
|
+
? ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS
|
|
1557
|
+
: ANTHROPIC_SMALL_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function normalizeStreamFailure(error: unknown): unknown {
|
|
1561
|
+
if (error instanceof Error) return error;
|
|
1562
|
+
if (error !== null && typeof error === "object") {
|
|
1563
|
+
// Structured rejections (e.g. `{ status, error, headers }` from an SDK or
|
|
1564
|
+
// injected client) carry transport metadata downstream classification
|
|
1565
|
+
// reads; wrap them in a mutable Error but copy every enumerable own
|
|
1566
|
+
// property so status/provider-code/header extraction still works.
|
|
1567
|
+
let message: string;
|
|
1568
|
+
try {
|
|
1569
|
+
message = JSON.stringify(error) || String(error);
|
|
1570
|
+
} catch {
|
|
1571
|
+
message = String(error);
|
|
1572
|
+
}
|
|
1573
|
+
const wrapper = new Error(message);
|
|
1574
|
+
Object.assign(wrapper, error as object);
|
|
1575
|
+
return wrapper;
|
|
1576
|
+
}
|
|
1577
|
+
// Primitive rejections (string/number/boolean/null/undefined): wrap with the
|
|
1578
|
+
// same string form downstream matchers already use (String(error)).
|
|
1579
|
+
return new Error(String(error));
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function attachAnthropicGraceFailureFacts(
|
|
1583
|
+
error: unknown,
|
|
1584
|
+
args: {
|
|
1585
|
+
elapsedMs: number;
|
|
1586
|
+
requestBytes: number;
|
|
1587
|
+
firstEventTimeoutMs: number | undefined;
|
|
1588
|
+
endpointClass: "canonical" | "custom";
|
|
1589
|
+
awaitingFirstEvent: boolean;
|
|
1590
|
+
},
|
|
1591
|
+
): void {
|
|
1592
|
+
if (
|
|
1593
|
+
!(error instanceof Error) ||
|
|
1594
|
+
!args.awaitingFirstEvent ||
|
|
1595
|
+
args.firstEventTimeoutMs === undefined ||
|
|
1596
|
+
args.firstEventTimeoutMs <= 0 ||
|
|
1597
|
+
args.elapsedMs < args.firstEventTimeoutMs ||
|
|
1598
|
+
args.endpointClass !== "custom" ||
|
|
1599
|
+
args.requestBytes < ANTHROPIC_LARGE_REQUEST_BYTES
|
|
1600
|
+
) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
Object.assign(error, {
|
|
1604
|
+
requestBytes: args.requestBytes,
|
|
1605
|
+
firstEventElapsedMs: args.elapsedMs,
|
|
1606
|
+
firstEventTimeoutMs: args.firstEventTimeoutMs,
|
|
1607
|
+
endpointClass: args.endpointClass,
|
|
1608
|
+
retryMaxAttempts: ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS,
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function createAnthropicFirstEventTimeoutError(args: {
|
|
1613
|
+
elapsedMs: number;
|
|
1614
|
+
requestBytes: number;
|
|
1615
|
+
firstEventTimeoutMs: number | undefined;
|
|
1616
|
+
endpointClass: "canonical" | "custom";
|
|
1617
|
+
/** Uploads this provider invocation already consumed before the timeout. */
|
|
1618
|
+
providerAttemptsConsumed?: number;
|
|
1619
|
+
}): FirstEventTimeoutError {
|
|
1620
|
+
const totalCeiling = resolveAnthropicFirstEventTimeoutMaxAttempts(args.requestBytes);
|
|
1621
|
+
// The session counts a whole provider invocation as one attempt, so the
|
|
1622
|
+
// ceiling handed up must bound TOTAL uploads across the invocation: subtract
|
|
1623
|
+
// the provider replays already spent inside this invocation. A small request
|
|
1624
|
+
// whose first upload 529'd and whose replay then timed out has already
|
|
1625
|
+
// consumed two uploads; reporting the full two-attempt ceiling would let the
|
|
1626
|
+
// session upload a third time.
|
|
1627
|
+
const retryMaxAttempts = Math.max(1, totalCeiling - (args.providerAttemptsConsumed ?? 0));
|
|
1628
|
+
const timeoutLabel = args.firstEventTimeoutMs === undefined ? "disabled" : `${args.firstEventTimeoutMs}ms`;
|
|
1629
|
+
return new FirstEventTimeoutError(
|
|
1630
|
+
`Anthropic stream timed out while waiting for the first event (elapsed=${args.elapsedMs}ms request_bytes=${args.requestBytes} endpoint=${args.endpointClass} configured_timeout=${timeoutLabel}; override with PI_STREAM_FIRST_EVENT_TIMEOUT_MS)`,
|
|
1631
|
+
{
|
|
1632
|
+
requestBytes: args.requestBytes,
|
|
1633
|
+
firstEventElapsedMs: args.elapsedMs,
|
|
1634
|
+
firstEventTimeoutMs: args.firstEventTimeoutMs,
|
|
1635
|
+
endpointClass: args.endpointClass,
|
|
1636
|
+
retryMaxAttempts,
|
|
1637
|
+
},
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Check if an error from the Anthropic SDK is a rate-limit/transient error that
|
|
1643
|
+
* should be retried before any content has been emitted.
|
|
1644
|
+
*
|
|
1645
|
+
* Includes malformed JSON stream-envelope parse errors seen from some
|
|
1646
|
+
* Anthropic-compatible proxy endpoints.
|
|
1647
|
+
*/
|
|
1648
|
+
/** Transient stream corruption errors where the response was truncated mid-JSON. */
|
|
1649
|
+
function isTransientStreamParseError(error: unknown): boolean {
|
|
1650
|
+
if (!(error instanceof Error)) return false;
|
|
1651
|
+
return /json parse error|unterminated string|unexpected end of json input/i.test(error.message);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
|
|
1655
|
+
|
|
1656
|
+
function createAnthropicStreamEnvelopeError(message: string): Error {
|
|
1657
|
+
return new Error(`${ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX} ${message}`);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES = new Set([
|
|
1661
|
+
"content_block_start",
|
|
1662
|
+
"content_block_delta",
|
|
1663
|
+
"content_block_stop",
|
|
1664
|
+
"message_delta",
|
|
1665
|
+
"message_stop",
|
|
1666
|
+
"message_start",
|
|
1667
|
+
]);
|
|
1668
|
+
|
|
1669
|
+
function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
|
|
1670
|
+
if (typeof eventType !== "string") return false;
|
|
1671
|
+
if (eventType === "ping") return true;
|
|
1672
|
+
return !ANTHROPIC_PRE_MESSAGE_START_EVENT_TYPES.has(eventType);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function createAnthropicStreamProgressPredicate(): (event: unknown) => boolean {
|
|
1676
|
+
let outputTokens = -1;
|
|
1677
|
+
|
|
1678
|
+
return event => {
|
|
1679
|
+
if (!isRecord(event) || typeof event.type !== "string") return false;
|
|
1680
|
+
if (
|
|
1681
|
+
event.type === "message_start" ||
|
|
1682
|
+
event.type === "content_block_start" ||
|
|
1683
|
+
event.type === "content_block_stop" ||
|
|
1684
|
+
event.type === "message_stop"
|
|
1685
|
+
) {
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
if (event.type === "content_block_delta") {
|
|
1689
|
+
if (!isRecord(event.delta)) return false;
|
|
1690
|
+
const delta = event.delta;
|
|
1691
|
+
return (
|
|
1692
|
+
(typeof delta.text === "string" && delta.text.length > 0) ||
|
|
1693
|
+
(typeof delta.thinking === "string" && delta.thinking.length > 0) ||
|
|
1694
|
+
(typeof delta.partial_json === "string" && delta.partial_json.length > 0) ||
|
|
1695
|
+
(typeof delta.signature === "string" && delta.signature.length > 0)
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (event.type === "message_delta") {
|
|
1699
|
+
if (isRecord(event.delta) && event.delta.stop_reason != null) return true;
|
|
1700
|
+
if (!isRecord(event.usage) || typeof event.usage.output_tokens !== "number") return false;
|
|
1701
|
+
if (event.usage.output_tokens <= outputTokens) return false;
|
|
1702
|
+
outputTokens = event.usage.output_tokens;
|
|
1703
|
+
return true;
|
|
1704
|
+
}
|
|
1705
|
+
return false;
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function isTransientStreamEnvelopeError(error: unknown): boolean {
|
|
1710
|
+
if (!(error instanceof Error)) return false;
|
|
1711
|
+
return (
|
|
1712
|
+
error.message.includes(ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX) ||
|
|
1713
|
+
/stream event order|before message_start|before terminal stop signal/i.test(error.message)
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
|
|
1718
|
+
if (!(error instanceof Error)) return false;
|
|
1719
|
+
return /stream event order|before message_start/i.test(error.message);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
export function isProviderRetryableError(error: unknown, provider?: string): boolean {
|
|
1723
|
+
if (!(error instanceof Error)) return false;
|
|
1724
|
+
if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
|
|
1725
|
+
const msg = error.message.toLowerCase();
|
|
1726
|
+
if (isUsageLimitError(error.message)) return false;
|
|
1727
|
+
if (
|
|
1728
|
+
isUnexpectedSocketCloseMessage(msg) ||
|
|
1729
|
+
/rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
|
|
1730
|
+
msg,
|
|
1731
|
+
) ||
|
|
1732
|
+
isTransientStreamParseError(error) ||
|
|
1733
|
+
isProviderRetryableStreamEnvelopeError(error)
|
|
1734
|
+
) {
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
return isRetryableError(error);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function createEmptyUsage(premiumRequests?: number): Usage {
|
|
1741
|
+
return {
|
|
1742
|
+
input: 0,
|
|
1743
|
+
output: 0,
|
|
1744
|
+
cacheRead: 0,
|
|
1745
|
+
cacheWrite: 0,
|
|
1746
|
+
totalTokens: 0,
|
|
1747
|
+
...(premiumRequests === undefined ? {} : { premiumRequests }),
|
|
1748
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export type AnthropicUsageLike = {
|
|
1753
|
+
cache_creation?: { ephemeral_5m_input_tokens?: number | null; ephemeral_1h_input_tokens?: number | null } | null;
|
|
1754
|
+
server_tool_use?: { web_search_requests?: number | null; web_fetch_requests?: number | null } | null;
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Capture Anthropic's optional cache-creation TTL breakdown and server-tool-use
|
|
1759
|
+
* counters into the harness Usage shape. Only sets fields that were reported, so
|
|
1760
|
+
* a `message_delta` that omits `cache_creation` does not clobber the breakdown
|
|
1761
|
+
* established at `message_start`.
|
|
1762
|
+
*/
|
|
1763
|
+
export function applyAnthropicUsageExtras(usage: Usage, source: AnthropicUsageLike): void {
|
|
1764
|
+
const cacheCreation = source.cache_creation;
|
|
1765
|
+
if (cacheCreation) {
|
|
1766
|
+
const fiveMinute = cacheCreation.ephemeral_5m_input_tokens ?? 0;
|
|
1767
|
+
const oneHour = cacheCreation.ephemeral_1h_input_tokens ?? 0;
|
|
1768
|
+
if (fiveMinute > 0 || oneHour > 0) {
|
|
1769
|
+
usage.cttl = {
|
|
1770
|
+
...(fiveMinute > 0 ? { ephemeral5m: fiveMinute } : {}),
|
|
1771
|
+
...(oneHour > 0 ? { ephemeral1h: oneHour } : {}),
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const serverToolUse = source.server_tool_use;
|
|
1776
|
+
if (serverToolUse) {
|
|
1777
|
+
const webSearch = serverToolUse.web_search_requests ?? 0;
|
|
1778
|
+
const webFetch = serverToolUse.web_fetch_requests ?? 0;
|
|
1779
|
+
if (webSearch > 0 || webFetch > 0) {
|
|
1780
|
+
usage.server = {
|
|
1781
|
+
...(webSearch > 0 ? { webSearch } : {}),
|
|
1782
|
+
...(webFetch > 0 ? { webFetch } : {}),
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Unique request-local tool whose wire name equals the parsed base, if any.
|
|
1790
|
+
* Only an exact, singular match is trusted; zero or multiple matches yield
|
|
1791
|
+
* `undefined` so the repair never guesses among ambiguous aliases.
|
|
1792
|
+
*/
|
|
1793
|
+
function resolveCpaCallableToolName(
|
|
1794
|
+
params: MessageCreateParamsStreaming,
|
|
1795
|
+
failure: CpaToolAliasRestoreFailure,
|
|
1796
|
+
): string | undefined {
|
|
1797
|
+
if (failure.baseName === undefined) return undefined;
|
|
1798
|
+
const tools = params.tools as Array<{ name?: string }> | undefined;
|
|
1799
|
+
if (!tools) return undefined;
|
|
1800
|
+
let match: string | undefined;
|
|
1801
|
+
for (const tool of tools) {
|
|
1802
|
+
if (tool.name !== failure.baseName) continue;
|
|
1803
|
+
if (match !== undefined) return undefined;
|
|
1804
|
+
match = tool.name;
|
|
1805
|
+
}
|
|
1806
|
+
return match;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Corrective steering for the one retry after a CPA alias-restore failure. A
|
|
1811
|
+
* provable unique callable name is stated deterministically; otherwise the
|
|
1812
|
+
* model is directed at tool discovery instead of being handed an invented
|
|
1813
|
+
* name, mirroring the agent loop's "not found → discover and activate"
|
|
1814
|
+
* guidance. The rejected alias is echoed verbatim so the model knows which
|
|
1815
|
+
* call was wrong; nothing else from the request is quoted.
|
|
1816
|
+
*/
|
|
1817
|
+
function buildCpaToolAliasSteering(failure: CpaToolAliasRestoreFailure, callableToolName?: string): string {
|
|
1818
|
+
const rejected = `Your previous tool call "${failure.alias}" was rejected by the Claude OAuth proxy: the tool name is not callable in this request.`;
|
|
1819
|
+
if (callableToolName !== undefined) {
|
|
1820
|
+
return `${rejected} The callable tool is "${callableToolName}". Call it by exactly that name; do not construct or reconstruct prefixed or aliased tool names.`;
|
|
1821
|
+
}
|
|
1822
|
+
return `${rejected} If you need this capability, call \`search_tool_bm25\` to discover and activate the matching tool, then retry.`;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Actionable terminal error for a CPA alias-restore failure that survived the
|
|
1827
|
+
* single corrective attempt. Deliberately statusless: no HTTP status, no
|
|
1828
|
+
* transport facts, and no recognizable status phrase, so neither the provider
|
|
1829
|
+
* generic 5xx retry nor the managed fallback controller re-sends the unchanged
|
|
1830
|
+
* request. Only the rejected alias and the deterministic callable name (when
|
|
1831
|
+
* provable) are quoted — never the request body or headers.
|
|
1832
|
+
*/
|
|
1833
|
+
function createCpaToolAliasTerminalError(failure: CpaToolAliasRestoreFailure, callableToolName?: string): Error {
|
|
1834
|
+
const base = `Claude OAuth proxy rejected tool call "${failure.alias}": the proxy cannot restore the Claude OAuth MCP tool alias (no unique request-local match), and the corrective retry was rejected again.`;
|
|
1835
|
+
const guidance =
|
|
1836
|
+
callableToolName !== undefined
|
|
1837
|
+
? ` The callable tool name is "${callableToolName}"; call it by exactly that name.`
|
|
1838
|
+
: ` No unique callable tool name could be determined; discover the correct tool name before retrying.`;
|
|
1839
|
+
return new Error(`${base}${guidance} The turn was not re-sent.`);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Stable identity for the logical turn currently being prompted: the
|
|
1844
|
+
* serialized content of the last user message. Fallback rebuilds of the same
|
|
1845
|
+
* turn keep the same fingerprint; the next user prompt changes it.
|
|
1846
|
+
*/
|
|
1847
|
+
function cpaTurnFingerprint(messages: Message[]): string {
|
|
1848
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1849
|
+
const message = messages[index];
|
|
1850
|
+
if (message.role !== "user") continue;
|
|
1851
|
+
const content = message.content;
|
|
1852
|
+
const serialized = typeof content === "string" ? content : JSON.stringify(content);
|
|
1853
|
+
return `${serialized.length}:${serialized}`;
|
|
1854
|
+
}
|
|
1855
|
+
return "";
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Append corrective steering as a trailing user turn, preserving role
|
|
1860
|
+
* alternation by merging into the last user message when it is already a user
|
|
1861
|
+
* turn.
|
|
1862
|
+
*/
|
|
1863
|
+
function appendCpaSteeringToMessages(params: MessageCreateParamsStreaming, text: string): void {
|
|
1864
|
+
const messages = params.messages as MessageParam[];
|
|
1865
|
+
const last = messages[messages.length - 1];
|
|
1866
|
+
if (last && last.role === "user") {
|
|
1867
|
+
last.content = Array.isArray(last.content)
|
|
1868
|
+
? [...last.content, { type: "text", text }]
|
|
1869
|
+
: `${last.content}\n\n${text}`;
|
|
1870
|
+
} else {
|
|
1871
|
+
messages.push({ role: "user", content: text });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
1876
|
+
model: Model<"anthropic-messages">,
|
|
1877
|
+
context: Context,
|
|
1878
|
+
options?: AnthropicOptions,
|
|
1879
|
+
): AssistantMessageEventStream => {
|
|
1880
|
+
const stream = new AssistantMessageEventStream();
|
|
1881
|
+
|
|
1882
|
+
(async () => {
|
|
1883
|
+
const startTime = Date.now();
|
|
1884
|
+
let firstTokenTime: number | undefined;
|
|
1885
|
+
|
|
1886
|
+
const copilotDynamicHeaders =
|
|
1887
|
+
model.provider === "github-copilot"
|
|
1888
|
+
? buildCopilotDynamicHeaders({
|
|
1889
|
+
messages: context.messages,
|
|
1890
|
+
hasImages: hasCopilotVisionInput(context.messages),
|
|
1891
|
+
premiumMultiplier: model.premiumMultiplier,
|
|
1892
|
+
headers: { ...(model.headers ?? {}), ...(options?.headers ?? {}) },
|
|
1893
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1894
|
+
})
|
|
1895
|
+
: undefined;
|
|
1896
|
+
const output: AssistantMessage = {
|
|
1897
|
+
role: "assistant",
|
|
1898
|
+
content: [],
|
|
1899
|
+
api: model.api as Api,
|
|
1900
|
+
provider: model.provider,
|
|
1901
|
+
model: model.id,
|
|
1902
|
+
usage: createEmptyUsage(copilotDynamicHeaders?.premiumRequests),
|
|
1903
|
+
stopReason: "stop",
|
|
1904
|
+
timestamp: Date.now(),
|
|
1905
|
+
};
|
|
1906
|
+
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
1907
|
+
let activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
1908
|
+
|
|
1909
|
+
try {
|
|
1910
|
+
let client: Anthropic;
|
|
1911
|
+
let isOAuthToken: boolean;
|
|
1912
|
+
|
|
1913
|
+
if (options?.client) {
|
|
1914
|
+
client = options.client;
|
|
1915
|
+
isOAuthToken = false;
|
|
1916
|
+
} else {
|
|
1917
|
+
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
|
1918
|
+
|
|
1919
|
+
const extraBetas = normalizeExtraBetas(options?.betas);
|
|
1920
|
+
const wantsAnthropicPriority = resolveServiceTier(options?.serviceTier, model.provider) === "priority";
|
|
1921
|
+
if (wantsAnthropicPriority && !extraBetas.includes(fastModeBeta)) {
|
|
1922
|
+
extraBetas.push(fastModeBeta);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const created = createClient(model, {
|
|
1926
|
+
model,
|
|
1927
|
+
apiKey,
|
|
1928
|
+
extraBetas,
|
|
1929
|
+
stream: true,
|
|
1930
|
+
interleavedThinking: options?.interleavedThinking ?? true,
|
|
1931
|
+
headers: options?.headers,
|
|
1932
|
+
dynamicHeaders: copilotDynamicHeaders?.headers,
|
|
1933
|
+
isOAuth: options?.isOAuth,
|
|
1934
|
+
hasTools: !!context.tools?.length,
|
|
1935
|
+
onSseEvent: options?.onSseEvent
|
|
1936
|
+
? event => options.onSseEvent!(event, model, options?.attemptScope)
|
|
1937
|
+
: undefined,
|
|
1938
|
+
fetch: options?.fetch,
|
|
1939
|
+
requestMaxRetries: options?.requestMaxRetries,
|
|
1940
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1941
|
+
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
1942
|
+
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
1943
|
+
});
|
|
1944
|
+
client = created.client;
|
|
1945
|
+
isOAuthToken = created.isOAuthToken;
|
|
1946
|
+
}
|
|
1947
|
+
const baseUrl =
|
|
1948
|
+
resolveAnthropicBaseUrl(model, options?.apiKey ?? getEnvApiKey(model.provider) ?? "") ??
|
|
1949
|
+
"https://api.anthropic.com";
|
|
1950
|
+
const providerSessionState = getAnthropicProviderSessionState(options?.providerSessionState);
|
|
1951
|
+
let disableStrictTools =
|
|
1952
|
+
(providerSessionState?.strictToolsDisabled ?? false) || (model.compat?.disableStrictTools ?? false);
|
|
1953
|
+
let strictFallbackErrorMessage: string | undefined;
|
|
1954
|
+
let dropFastMode = providerSessionState?.fastModeDisabled ?? false;
|
|
1955
|
+
let droppedForcedToolChoice = false;
|
|
1956
|
+
// Exactly one corrective retry per request for the CPA alias-restore
|
|
1957
|
+
// signature (issue #4338); recurrence terminalizes instead of resending.
|
|
1958
|
+
let cpaAliasRepairApplied = false;
|
|
1959
|
+
let thinkingReplayRepairScope: AnthropicThinkingReplayRepairScope =
|
|
1960
|
+
providerSessionState?.thinkingReplayRepairScope ?? "none";
|
|
1961
|
+
let thinkingReplayRepairAttempts = providerSessionState?.thinkingReplayRepairAttempts ?? 0;
|
|
1962
|
+
// A scope inherited from an earlier turn can only have come from the
|
|
1963
|
+
// deterministic branch below — the speculative masked-`api_error` probe is
|
|
1964
|
+
// never persisted — so a completed stream must not release it.
|
|
1965
|
+
let thinkingReplayRepairPersistent = thinkingReplayRepairScope !== "none";
|
|
1966
|
+
let generatedCacheBudget: GeneratedCacheBudget = providerSessionState?.generatedCacheBudget ?? 2;
|
|
1967
|
+
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
1968
|
+
// Degradation state is cumulative: every fallback rebuild must merge all
|
|
1969
|
+
// repairs activated so far. Rebuilding from only the immediate call lets
|
|
1970
|
+
// a later strict/forced-tool/fast-mode fallback reintroduce the rejected
|
|
1971
|
+
// shape (e.g. invalid thinking signatures or forced tool_choice), and
|
|
1972
|
+
// the one-shot thinking-repair guard then blocks recovery.
|
|
1973
|
+
let nextParams = buildParams(
|
|
1974
|
+
model,
|
|
1975
|
+
baseUrl,
|
|
1976
|
+
context,
|
|
1977
|
+
isOAuthToken,
|
|
1978
|
+
options,
|
|
1979
|
+
disableStrictTools,
|
|
1980
|
+
{
|
|
1981
|
+
repairLatestAssistantThinking: thinkingReplayRepairScope === "latest",
|
|
1982
|
+
repairAllAssistantThinking: thinkingReplayRepairScope === "all",
|
|
1983
|
+
},
|
|
1984
|
+
generatedCacheBudget,
|
|
1985
|
+
);
|
|
1986
|
+
if (droppedForcedToolChoice) {
|
|
1987
|
+
delete nextParams.tool_choice;
|
|
1988
|
+
}
|
|
1989
|
+
if (disableStrictTools) {
|
|
1990
|
+
dropAnthropicStrictTools(nextParams);
|
|
1991
|
+
}
|
|
1992
|
+
if (dropFastMode) {
|
|
1993
|
+
dropAnthropicFastMode(nextParams);
|
|
1994
|
+
}
|
|
1995
|
+
const replacementPayload = await options?.onPayload?.(nextParams, model, options?.attemptScope);
|
|
1996
|
+
if (replacementPayload !== undefined) {
|
|
1997
|
+
nextParams = replacementPayload as typeof nextParams;
|
|
1998
|
+
}
|
|
1999
|
+
// Managed-mode CPA steering (issue #4338): a previous managed attempt of
|
|
2000
|
+
// this exact turn recorded a corrective tool-name message. Apply it only
|
|
2001
|
+
// while the turn is unchanged; a different user prompt expires it so a
|
|
2002
|
+
// stale correction never leaks into a later turn.
|
|
2003
|
+
const cpaSteering = providerSessionState?.cpaToolAliasSteering;
|
|
2004
|
+
if (cpaSteering) {
|
|
2005
|
+
if (cpaSteering.turnFingerprint === cpaTurnFingerprint(context.messages)) {
|
|
2006
|
+
appendCpaSteeringToMessages(nextParams, cpaSteering.message);
|
|
2007
|
+
} else {
|
|
2008
|
+
providerSessionState.cpaToolAliasSteering = undefined;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
validateCacheControls(nextParams as AnthropicCacheParams);
|
|
2012
|
+
return nextParams;
|
|
2013
|
+
};
|
|
2014
|
+
let params = await prepareParams();
|
|
2015
|
+
const setRawRequestDump = (body: MessageCreateParamsStreaming): void => {
|
|
2016
|
+
rawRequestDump = {
|
|
2017
|
+
provider: model.provider,
|
|
2018
|
+
api: output.api,
|
|
2019
|
+
model: model.id,
|
|
2020
|
+
method: "POST",
|
|
2021
|
+
url: `${baseUrl}/v1/messages`,
|
|
2022
|
+
body,
|
|
2023
|
+
};
|
|
2024
|
+
};
|
|
2025
|
+
setRawRequestDump(params);
|
|
2026
|
+
const inheritedRejectedPayload = providerSessionState?.thinkingReplayRejectedPayload;
|
|
2027
|
+
if (
|
|
2028
|
+
inheritedRejectedPayload &&
|
|
2029
|
+
thinkingReplayRepairScope !== "none" &&
|
|
2030
|
+
!anthropicPayloadChanged(inheritedRejectedPayload, fingerprintAnthropicPayload(params))
|
|
2031
|
+
) {
|
|
2032
|
+
throw createAnthropicThinkingRepairNoopError(
|
|
2033
|
+
new Error(
|
|
2034
|
+
"invalid_request_error: persisted Anthropic thinking repair did not change the outgoing payload",
|
|
2035
|
+
),
|
|
2036
|
+
params,
|
|
2037
|
+
inheritedRejectedPayload,
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
if (providerSessionState?.thinkingReplayRejectedPayload) {
|
|
2041
|
+
providerSessionState.thinkingReplayRejectedPayload = undefined;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
type Block = (
|
|
2045
|
+
| ThinkingContent
|
|
2046
|
+
| RedactedThinkingContent
|
|
2047
|
+
| TextContent
|
|
2048
|
+
| (ToolCall & { partialJson: string })
|
|
2049
|
+
) & { index: number };
|
|
2050
|
+
const blocks = output.content as Block[];
|
|
2051
|
+
const blocksByAnthropicIndex = new Map<number, Block>();
|
|
2052
|
+
const truncatedToolCalls = new Set<ToolCall>();
|
|
2053
|
+
// Bounded diagnostic for degraded primitive increments: at most one
|
|
2054
|
+
// warning per delta type per stream invocation, naming only the
|
|
2055
|
+
// envelope shape (delta type and received typeof) — never the payload.
|
|
2056
|
+
const degradedIncrementDiagnostics = new Set<string>();
|
|
2057
|
+
const noteDegradedIncrement = (deltaType: string, received: unknown): void => {
|
|
2058
|
+
if (degradedIncrementDiagnostics.has(deltaType)) return;
|
|
2059
|
+
degradedIncrementDiagnostics.add(deltaType);
|
|
2060
|
+
logger.warn("anthropic: degraded non-string stream increment to empty string", {
|
|
2061
|
+
model: model.id,
|
|
2062
|
+
provider: model.provider,
|
|
2063
|
+
deltaType,
|
|
2064
|
+
receivedType: received === null ? "null" : typeof received,
|
|
2065
|
+
});
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// Derive from the ACTUAL request shape, not the option default: the request
|
|
2069
|
+
// only sends `display: "summarized"` on specific paths (adaptive display is
|
|
2070
|
+
// omitted for models where supportsAdaptiveThinkingDisplay is false). Defaulting
|
|
2071
|
+
// to summarized would mislabel raw thinking as a provider-displayable summary.
|
|
2072
|
+
const summarizedThinking =
|
|
2073
|
+
(params.thinking as { display?: AnthropicThinkingDisplay } | undefined)?.display === "summarized";
|
|
2074
|
+
const reasoningBuffers = new WeakMap<object, string>();
|
|
2075
|
+
const getBlockByAnthropicIndex = (anthropicIndex: number) => {
|
|
2076
|
+
const block = blocksByAnthropicIndex.get(anthropicIndex);
|
|
2077
|
+
if (!block) return { block: undefined, contentIndex: -1 };
|
|
2078
|
+
return { block, contentIndex: blocks.indexOf(block) };
|
|
2079
|
+
};
|
|
2080
|
+
const trackBlockByAnthropicIndex = (anthropicIndex: number, block: Block) => {
|
|
2081
|
+
const orphaned = blocksByAnthropicIndex.get(anthropicIndex);
|
|
2082
|
+
if (orphaned) {
|
|
2083
|
+
if (orphaned.type === "toolCall") {
|
|
2084
|
+
orphaned.incompleteArguments = true;
|
|
2085
|
+
orphaned.incompleteArgumentsReason = "ambiguous";
|
|
2086
|
+
truncatedToolCalls.add(orphaned);
|
|
2087
|
+
}
|
|
2088
|
+
if (block.type === "toolCall") {
|
|
2089
|
+
block.incompleteArguments = true;
|
|
2090
|
+
block.incompleteArgumentsReason = "ambiguous";
|
|
2091
|
+
}
|
|
2092
|
+
throw new Error("Anthropic stream reused an active content block index");
|
|
2093
|
+
}
|
|
2094
|
+
blocksByAnthropicIndex.set(anthropicIndex, block);
|
|
2095
|
+
};
|
|
2096
|
+
const resetOutputForRetry = () => {
|
|
2097
|
+
output.content.length = 0;
|
|
2098
|
+
output.responseId = undefined;
|
|
2099
|
+
output.errorKind = undefined;
|
|
2100
|
+
output.errorStatus = undefined;
|
|
2101
|
+
output.errorMessage = strictFallbackErrorMessage;
|
|
2102
|
+
output.providerPayload = undefined;
|
|
2103
|
+
output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
|
|
2104
|
+
output.stopReason = "stop";
|
|
2105
|
+
firstTokenTime = undefined;
|
|
2106
|
+
truncatedToolCalls.clear();
|
|
2107
|
+
};
|
|
2108
|
+
const idleTimeoutMs =
|
|
2109
|
+
options?.streamIdleTimeoutMs ??
|
|
2110
|
+
getStreamIdleTimeoutMs(getProviderStreamIdleTimeoutFallbackMs(model.provider));
|
|
2111
|
+
const firstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(model.provider);
|
|
2112
|
+
const firstEventTimeoutMs =
|
|
2113
|
+
options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs, firstEventFallbackMs);
|
|
2114
|
+
const endpointClass = classifyAnthropicEndpoint(options?.client?.baseURL ?? baseUrl);
|
|
2115
|
+
stream.push({ type: "start", partial: output });
|
|
2116
|
+
// Retry loop for transient errors from the stream.
|
|
2117
|
+
// Provider-level transport/rate-limit failures: only before any streamed content starts.
|
|
2118
|
+
// Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
|
|
2119
|
+
let providerRetryAttempt = 0;
|
|
2120
|
+
// Total uploads this invocation has spent, including corrective-policy
|
|
2121
|
+
// replays (strict-tool/forced-tool/fast-mode/thinking/CPA) that reset
|
|
2122
|
+
// providerRetryAttempt before `continue`. The timeout ceiling must bound
|
|
2123
|
+
// TOTAL uploads, so it reads this counter, not the resettable one.
|
|
2124
|
+
let providerUploadCount = 0;
|
|
2125
|
+
while (true) {
|
|
2126
|
+
let firstEventWaitStartedAt: number | undefined;
|
|
2127
|
+
const requestBytes = fingerprintAnthropicPayload(params).bytes;
|
|
2128
|
+
const firstEventWatchdogMs = resolveAnthropicFirstEventWatchdogMs(
|
|
2129
|
+
firstEventTimeoutMs,
|
|
2130
|
+
endpointClass,
|
|
2131
|
+
requestBytes,
|
|
2132
|
+
);
|
|
2133
|
+
const requestUploadCeilingBound =
|
|
2134
|
+
endpointClass === "custom" &&
|
|
2135
|
+
requestBytes >= ANTHROPIC_LARGE_REQUEST_BYTES &&
|
|
2136
|
+
firstEventTimeoutMs !== undefined &&
|
|
2137
|
+
firstEventTimeoutMs > 0;
|
|
2138
|
+
// Retries reset output.content; drop stale block correlations from the aborted attempt.
|
|
2139
|
+
blocksByAnthropicIndex.clear();
|
|
2140
|
+
truncatedToolCalls.clear();
|
|
2141
|
+
activeAbortTracker = createAbortSourceTracker(options?.signal);
|
|
2142
|
+
let firstEventTimeoutAbortError: FirstEventTimeoutError | undefined;
|
|
2143
|
+
const idleTimeoutAbortError = new Error("Anthropic stream stalled while waiting for the next event");
|
|
2144
|
+
const { requestSignal } = activeAbortTracker;
|
|
2145
|
+
setRawRequestDump(params);
|
|
2146
|
+
options?.onStreamCreated?.();
|
|
2147
|
+
const anthropicRequest = client.messages.create(
|
|
2148
|
+
{ ...params, stream: true },
|
|
2149
|
+
{
|
|
2150
|
+
signal: requestSignal,
|
|
2151
|
+
...(requestUploadCeilingBound ? { maxRetries: 0 } : {}),
|
|
2152
|
+
},
|
|
2153
|
+
);
|
|
2154
|
+
let streamedReplayUnsafeContent = false;
|
|
2155
|
+
let sawProviderSafetyStop = false;
|
|
2156
|
+
let sawFirstSemanticEvent = false;
|
|
2157
|
+
|
|
2158
|
+
try {
|
|
2159
|
+
const {
|
|
2160
|
+
events: anthropicStream,
|
|
2161
|
+
response,
|
|
2162
|
+
requestId,
|
|
2163
|
+
} = await getAnthropicStreamResponse(
|
|
2164
|
+
anthropicRequest,
|
|
2165
|
+
requestSignal,
|
|
2166
|
+
options?.client ? event => options?.onSseEvent?.(event, model, options?.attemptScope) : undefined,
|
|
2167
|
+
);
|
|
2168
|
+
await notifyProviderResponse(options, response, model, requestId);
|
|
2169
|
+
firstEventWaitStartedAt = Date.now();
|
|
2170
|
+
let sawEvent = false;
|
|
2171
|
+
let sawMessageStart = false;
|
|
2172
|
+
let sawTerminalEnvelope = false;
|
|
2173
|
+
let sawMessageStop = false;
|
|
2174
|
+
const isProgressEvent = createAnthropicStreamProgressPredicate();
|
|
2175
|
+
|
|
2176
|
+
for await (const event of iterateWithIdleTimeout(anthropicStream, {
|
|
2177
|
+
idleTimeoutMs,
|
|
2178
|
+
firstItemTimeoutMs: firstEventWatchdogMs,
|
|
2179
|
+
errorMessage: idleTimeoutAbortError.message,
|
|
2180
|
+
firstItemErrorMessage: "Anthropic stream timed out while waiting for the first event",
|
|
2181
|
+
onIdle: () => activeAbortTracker.abortLocally(idleTimeoutAbortError),
|
|
2182
|
+
onFirstItemTimeout: () => {
|
|
2183
|
+
firstEventTimeoutAbortError = createAnthropicFirstEventTimeoutError({
|
|
2184
|
+
elapsedMs: Date.now() - (firstEventWaitStartedAt ?? Date.now()),
|
|
2185
|
+
requestBytes,
|
|
2186
|
+
firstEventTimeoutMs,
|
|
2187
|
+
endpointClass,
|
|
2188
|
+
providerAttemptsConsumed: providerUploadCount,
|
|
2189
|
+
});
|
|
2190
|
+
activeAbortTracker.abortLocally(firstEventTimeoutAbortError);
|
|
2191
|
+
},
|
|
2192
|
+
abortSignal: options?.signal,
|
|
2193
|
+
isProgressItem: event => {
|
|
2194
|
+
if (!isRecord(event) || (!sawMessageStart && event.type !== "message_start")) return false;
|
|
2195
|
+
const progress = isProgressEvent(event);
|
|
2196
|
+
if (progress) sawFirstSemanticEvent = true;
|
|
2197
|
+
return progress;
|
|
2198
|
+
},
|
|
2199
|
+
})) {
|
|
2200
|
+
sawEvent = true;
|
|
2201
|
+
if (sawMessageStop) {
|
|
2202
|
+
throw createAnthropicStreamEnvelopeError("received event after message_stop");
|
|
2203
|
+
}
|
|
2204
|
+
if (sawProviderSafetyStop) {
|
|
2205
|
+
if (event.type === "message_stop") {
|
|
2206
|
+
sawTerminalEnvelope = true;
|
|
2207
|
+
sawMessageStop = true;
|
|
2208
|
+
}
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (event.type === "message_start") {
|
|
2213
|
+
if (sawMessageStart) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
sawMessageStart = true;
|
|
2217
|
+
applyAnthropicUsageExtras(output.usage, event.message.usage);
|
|
2218
|
+
output.responseId = event.message.id;
|
|
2219
|
+
output.usage.input = event.message.usage.input_tokens || 0;
|
|
2220
|
+
output.usage.output = event.message.usage.output_tokens || 0;
|
|
2221
|
+
output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;
|
|
2222
|
+
output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;
|
|
2223
|
+
output.usage.totalTokens =
|
|
2224
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
2225
|
+
calculateCost(model, output.usage);
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if (!sawMessageStart) {
|
|
2230
|
+
if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
throw createAnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if (event.type === "content_block_start") {
|
|
2237
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
2238
|
+
if (event.content_block.type === "text") {
|
|
2239
|
+
streamedReplayUnsafeContent = true;
|
|
2240
|
+
const block: Block = {
|
|
2241
|
+
type: "text",
|
|
2242
|
+
text: "",
|
|
2243
|
+
index: event.index,
|
|
2244
|
+
};
|
|
2245
|
+
output.content.push(block);
|
|
2246
|
+
trackBlockByAnthropicIndex(event.index, block);
|
|
2247
|
+
stream.push({
|
|
2248
|
+
type: "text_start",
|
|
2249
|
+
contentIndex: output.content.length - 1,
|
|
2250
|
+
partial: output,
|
|
2251
|
+
});
|
|
2252
|
+
} else if (event.content_block.type === "thinking") {
|
|
2253
|
+
const block: Block = {
|
|
2254
|
+
type: "thinking",
|
|
2255
|
+
thinking: "",
|
|
2256
|
+
thinkingSignature: "",
|
|
2257
|
+
index: event.index,
|
|
2258
|
+
};
|
|
2259
|
+
output.content.push(block);
|
|
2260
|
+
trackBlockByAnthropicIndex(event.index, block);
|
|
2261
|
+
// Emit thinking_start FIRST so a reasoning item is open before any
|
|
2262
|
+
// summary-start: the Responses SSE encoder only accepts a summary
|
|
2263
|
+
// start when state.open.kind === "reasoning", otherwise the
|
|
2264
|
+
// reasoning_summary_part.added frame is dropped and deltas arrive
|
|
2265
|
+
// out of order.
|
|
2266
|
+
stream.push({
|
|
2267
|
+
type: "thinking_start",
|
|
2268
|
+
contentIndex: output.content.length - 1,
|
|
2269
|
+
partial: output,
|
|
2270
|
+
});
|
|
2271
|
+
if (summarizedThinking) {
|
|
2272
|
+
reasoningBuffers.set(block, "");
|
|
2273
|
+
stream.push({
|
|
2274
|
+
type: "reasoning_summary_start",
|
|
2275
|
+
contentIndex: output.content.length - 1,
|
|
2276
|
+
partial: output,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
} else if (event.content_block.type === "redacted_thinking") {
|
|
2280
|
+
const block: Block = {
|
|
2281
|
+
type: "redactedThinking",
|
|
2282
|
+
data: event.content_block.data,
|
|
2283
|
+
index: event.index,
|
|
2284
|
+
};
|
|
2285
|
+
output.content.push(block);
|
|
2286
|
+
trackBlockByAnthropicIndex(event.index, block);
|
|
2287
|
+
} else if (event.content_block.type === "tool_use") {
|
|
2288
|
+
streamedReplayUnsafeContent = true;
|
|
2289
|
+
const initialArguments: unknown = event.content_block.input;
|
|
2290
|
+
if (
|
|
2291
|
+
initialArguments === null ||
|
|
2292
|
+
typeof initialArguments !== "object" ||
|
|
2293
|
+
Array.isArray(initialArguments)
|
|
2294
|
+
) {
|
|
2295
|
+
throw new Error("Anthropic tool_use started with non-object arguments");
|
|
2296
|
+
}
|
|
2297
|
+
const block: Block = {
|
|
2298
|
+
type: "toolCall",
|
|
2299
|
+
id: event.content_block.id,
|
|
2300
|
+
name: isOAuthToken
|
|
2301
|
+
? stripClaudeToolPrefix(event.content_block.name)
|
|
2302
|
+
: event.content_block.name,
|
|
2303
|
+
arguments: initialArguments as Record<string, unknown>,
|
|
2304
|
+
partialJson: "",
|
|
2305
|
+
index: event.index,
|
|
2306
|
+
};
|
|
2307
|
+
output.content.push(block);
|
|
2308
|
+
trackBlockByAnthropicIndex(event.index, block);
|
|
2309
|
+
stream.push({
|
|
2310
|
+
type: "toolcall_start",
|
|
2311
|
+
contentIndex: output.content.length - 1,
|
|
2312
|
+
partial: output,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
} else if (event.type === "content_block_delta") {
|
|
2316
|
+
if (event.delta.type === "text_delta") {
|
|
2317
|
+
const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
|
|
2318
|
+
if (block && block.type === "text") {
|
|
2319
|
+
const rawTextDelta: unknown = event.delta.text;
|
|
2320
|
+
if (typeof rawTextDelta !== "string") {
|
|
2321
|
+
noteDegradedIncrement("text_delta", rawTextDelta);
|
|
2322
|
+
}
|
|
2323
|
+
const textDelta = typeof rawTextDelta === "string" ? rawTextDelta : "";
|
|
2324
|
+
block.text += textDelta;
|
|
2325
|
+
stream.push({
|
|
2326
|
+
type: "text_delta",
|
|
2327
|
+
contentIndex: index,
|
|
2328
|
+
delta: textDelta,
|
|
2329
|
+
partial: output,
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
} else if (event.delta.type === "thinking_delta") {
|
|
2333
|
+
const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
|
|
2334
|
+
if (block && block.type === "thinking") {
|
|
2335
|
+
const rawThinkingDelta: unknown = event.delta.thinking;
|
|
2336
|
+
if (typeof rawThinkingDelta !== "string") {
|
|
2337
|
+
noteDegradedIncrement("thinking_delta", rawThinkingDelta);
|
|
2338
|
+
}
|
|
2339
|
+
const thinkingDelta = typeof rawThinkingDelta === "string" ? rawThinkingDelta : "";
|
|
2340
|
+
block.thinking += thinkingDelta;
|
|
2341
|
+
if (summarizedThinking) {
|
|
2342
|
+
const summary = (reasoningBuffers.get(block) ?? "") + thinkingDelta;
|
|
2343
|
+
reasoningBuffers.set(block, summary);
|
|
2344
|
+
stream.push({
|
|
2345
|
+
type: "reasoning_summary_delta",
|
|
2346
|
+
contentIndex: index,
|
|
2347
|
+
delta: thinkingDelta,
|
|
2348
|
+
partial: output,
|
|
2349
|
+
});
|
|
2350
|
+
} else {
|
|
2351
|
+
stream.push({
|
|
2352
|
+
type: "thinking_delta",
|
|
2353
|
+
contentIndex: index,
|
|
2354
|
+
delta: thinkingDelta,
|
|
2355
|
+
partial: output,
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
2360
|
+
const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
|
|
2361
|
+
if (block && block.type === "toolCall") {
|
|
2362
|
+
const rawJsonDelta: unknown = event.delta.partial_json;
|
|
2363
|
+
if (typeof rawJsonDelta !== "string") {
|
|
2364
|
+
// Tool-argument fragments are positional JSON text: erasing or
|
|
2365
|
+
// coercing any malformed increment (primitive OR object/function)
|
|
2366
|
+
// assembles valid-but-wrong arguments — e.g. `{"n":1` + numeric
|
|
2367
|
+
// primitive erased to "" + `3}` parses as {"n":13} and executes.
|
|
2368
|
+
// Prose/thinking/signature anomalies are safe to degrade; tool
|
|
2369
|
+
// arguments fail the turn closed. The payload never enters the
|
|
2370
|
+
// error.
|
|
2371
|
+
throw new Error(
|
|
2372
|
+
"Anthropic stream sent a non-string input_json_delta tool-argument increment; failing the turn instead of assembling wrong tool arguments",
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
const jsonDelta = rawJsonDelta;
|
|
2376
|
+
block.partialJson += jsonDelta;
|
|
2377
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
2378
|
+
stream.push({
|
|
2379
|
+
type: "toolcall_delta",
|
|
2380
|
+
contentIndex: index,
|
|
2381
|
+
delta: jsonDelta,
|
|
2382
|
+
partial: output,
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
} else if (event.delta.type === "signature_delta") {
|
|
2386
|
+
const { block } = getBlockByAnthropicIndex(event.index);
|
|
2387
|
+
if (block && block.type === "thinking") {
|
|
2388
|
+
block.thinkingSignature = block.thinkingSignature || "";
|
|
2389
|
+
const rawSignatureDelta: unknown = event.delta.signature;
|
|
2390
|
+
if (typeof rawSignatureDelta === "string") {
|
|
2391
|
+
block.thinkingSignature += rawSignatureDelta;
|
|
2392
|
+
} else {
|
|
2393
|
+
noteDegradedIncrement("signature_delta", rawSignatureDelta);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
} else if (event.type === "content_block_stop") {
|
|
2398
|
+
const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
|
|
2399
|
+
if (block) {
|
|
2400
|
+
blocksByAnthropicIndex.delete(event.index);
|
|
2401
|
+
delete (block as { index?: number }).index;
|
|
2402
|
+
if (block.type === "text") {
|
|
2403
|
+
stream.push({
|
|
2404
|
+
type: "text_end",
|
|
2405
|
+
contentIndex: index,
|
|
2406
|
+
content: block.text,
|
|
2407
|
+
partial: output,
|
|
2408
|
+
});
|
|
2409
|
+
} else if (block.type === "thinking") {
|
|
2410
|
+
if (summarizedThinking) {
|
|
2411
|
+
const summaryText = reasoningBuffers.get(block) ?? "";
|
|
2412
|
+
const mutable = block as {
|
|
2413
|
+
provenance?: "summary" | "raw" | "mixed";
|
|
2414
|
+
summaryText?: string;
|
|
2415
|
+
};
|
|
2416
|
+
if (mutable.summaryText === undefined) mutable.summaryText = summaryText;
|
|
2417
|
+
if (mutable.provenance === undefined) mutable.provenance = "summary";
|
|
2418
|
+
stream.push({
|
|
2419
|
+
type: "reasoning_summary_end",
|
|
2420
|
+
contentIndex: index,
|
|
2421
|
+
content: summaryText,
|
|
2422
|
+
partial: output,
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
stream.push({
|
|
2426
|
+
type: "thinking_end",
|
|
2427
|
+
contentIndex: index,
|
|
2428
|
+
content: block.thinking,
|
|
2429
|
+
partial: output,
|
|
2430
|
+
});
|
|
2431
|
+
} else if (block.type === "toolCall") {
|
|
2432
|
+
if (!isCompleteJson(block.partialJson)) {
|
|
2433
|
+
truncatedToolCalls.add(block);
|
|
2434
|
+
block.incompleteArguments = true;
|
|
2435
|
+
block.incompleteArgumentsReason = "truncated";
|
|
2436
|
+
}
|
|
2437
|
+
if (block.partialJson.trim()) {
|
|
2438
|
+
const parsedArguments: unknown = parseStreamingJson(block.partialJson);
|
|
2439
|
+
if (
|
|
2440
|
+
parsedArguments === null ||
|
|
2441
|
+
typeof parsedArguments !== "object" ||
|
|
2442
|
+
Array.isArray(parsedArguments)
|
|
2443
|
+
) {
|
|
2444
|
+
throw new Error("Anthropic tool_use completed with non-object arguments");
|
|
2445
|
+
}
|
|
2446
|
+
block.arguments = parsedArguments as Record<string, unknown>;
|
|
2447
|
+
captureUnicodeEscapeEvidence(block, block.partialJson);
|
|
2448
|
+
}
|
|
2449
|
+
delete (block as { partialJson?: string }).partialJson;
|
|
2450
|
+
stream.push({
|
|
2451
|
+
type: "toolcall_end",
|
|
2452
|
+
contentIndex: index,
|
|
2453
|
+
toolCall: block,
|
|
2454
|
+
partial: output,
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
} else if (event.type === "message_delta") {
|
|
2459
|
+
const rawStopReason = event.delta.stop_reason as string | null | undefined;
|
|
2460
|
+
const stopDetails = event.delta.stop_details;
|
|
2461
|
+
const isProviderSafetyStop =
|
|
2462
|
+
rawStopReason === "refusal" ||
|
|
2463
|
+
rawStopReason === "sensitive" ||
|
|
2464
|
+
stopDetails?.type === "refusal" ||
|
|
2465
|
+
stopDetails?.type === "sensitive";
|
|
2466
|
+
if (rawStopReason) {
|
|
2467
|
+
output.stopReason = isProviderSafetyStop ? "error" : mapStopReason(rawStopReason);
|
|
2468
|
+
sawTerminalEnvelope = true;
|
|
2469
|
+
}
|
|
2470
|
+
if (isProviderSafetyStop) {
|
|
2471
|
+
sawProviderSafetyStop = true;
|
|
2472
|
+
sawTerminalEnvelope = true;
|
|
2473
|
+
output.stopReason = "error";
|
|
2474
|
+
// Mint the terminal kind with adapter provenance: the
|
|
2475
|
+
// structured refusal signal was parsed from the stream
|
|
2476
|
+
// delta, so the mark (not the wire field) carries the
|
|
2477
|
+
// authority (#4777).
|
|
2478
|
+
const authenticated = mintProviderSafetyStop(
|
|
2479
|
+
output,
|
|
2480
|
+
stopDetails?.type === "refusal" || stopDetails?.type === "sensitive"
|
|
2481
|
+
? stopDetails.type
|
|
2482
|
+
: rawStopReason === "sensitive"
|
|
2483
|
+
? "sensitive"
|
|
2484
|
+
: "refusal",
|
|
2485
|
+
PROVIDER_SAFETY_STOP_ADAPTER_CAPABILITY,
|
|
2486
|
+
options?.fetch ?? options?.client,
|
|
2487
|
+
isProviderSafetyStopAdapterInvocation(options),
|
|
2488
|
+
);
|
|
2489
|
+
if (!authenticated) {
|
|
2490
|
+
output.transportFailure = {
|
|
2491
|
+
kind: "transport",
|
|
2492
|
+
status: 500,
|
|
2493
|
+
providerCode: "untrusted_safety_stop",
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
if (stopDetails?.type === "refusal") {
|
|
2497
|
+
const explanation = stopDetails.explanation?.trim();
|
|
2498
|
+
const category = stopDetails.category;
|
|
2499
|
+
const label = category ? `Refusal (${category})` : "Refusal";
|
|
2500
|
+
output.errorMessage = explanation ? `${label}: ${explanation}` : label;
|
|
2501
|
+
} else if (!output.errorMessage) {
|
|
2502
|
+
output.errorMessage =
|
|
2503
|
+
rawStopReason === "refusal"
|
|
2504
|
+
? "Refusal (no details provided)"
|
|
2505
|
+
: "Content flagged by safety filters";
|
|
2506
|
+
}
|
|
2507
|
+
} else if (output.stopReason === "error" && !output.errorMessage) {
|
|
2508
|
+
// Anthropic flagged an error-class stop without populating stop_details.
|
|
2509
|
+
// Surface the raw reason instead of falling through to the generic
|
|
2510
|
+
// "unknown error" string when we throw below.
|
|
2511
|
+
output.errorMessage = `Anthropic stream ended with stop_reason: ${rawStopReason ?? "unknown"}`;
|
|
2512
|
+
}
|
|
2513
|
+
if (event.usage.input_tokens != null) {
|
|
2514
|
+
output.usage.input = event.usage.input_tokens;
|
|
2515
|
+
}
|
|
2516
|
+
if (event.usage.output_tokens != null) {
|
|
2517
|
+
output.usage.output = event.usage.output_tokens;
|
|
2518
|
+
}
|
|
2519
|
+
if (event.usage.cache_read_input_tokens != null) {
|
|
2520
|
+
output.usage.cacheRead = event.usage.cache_read_input_tokens;
|
|
2521
|
+
}
|
|
2522
|
+
if (event.usage.cache_creation_input_tokens != null) {
|
|
2523
|
+
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
|
|
2524
|
+
}
|
|
2525
|
+
applyAnthropicUsageExtras(output.usage, event.usage);
|
|
2526
|
+
output.usage.totalTokens =
|
|
2527
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
2528
|
+
calculateCost(model, output.usage);
|
|
2529
|
+
} else if (event.type === "message_stop") {
|
|
2530
|
+
sawTerminalEnvelope = true;
|
|
2531
|
+
sawMessageStop = true;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
|
|
2536
|
+
if (firstEventTimeoutError) {
|
|
2537
|
+
throw firstEventTimeoutError;
|
|
2538
|
+
}
|
|
2539
|
+
if (activeAbortTracker.wasCallerAbort()) {
|
|
2540
|
+
throw new Error("Request was aborted");
|
|
2541
|
+
}
|
|
2542
|
+
if (!sawEvent || !sawMessageStart) {
|
|
2543
|
+
throw createAnthropicStreamEnvelopeError("stream ended before message_start");
|
|
2544
|
+
}
|
|
2545
|
+
if (!sawTerminalEnvelope) {
|
|
2546
|
+
throw createAnthropicStreamEnvelopeError("stream ended before terminal stop signal");
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
2550
|
+
throw new Error(output.errorMessage ?? "An unknown error occurred");
|
|
2551
|
+
}
|
|
2552
|
+
// The first stream that completes is the only evidence available that this
|
|
2553
|
+
// session is not the #4011 loop, which never produced one. Release the
|
|
2554
|
+
// repair escalation and the budget it consumed: the masked `api_error`
|
|
2555
|
+
// branch above fires on an error nobody can classify, so keeping its
|
|
2556
|
+
// guess would silently strip native thinking replay from every later
|
|
2557
|
+
// turn of the session over what may have been one transient blip.
|
|
2558
|
+
//
|
|
2559
|
+
// A deterministic rejection ("cannot be modified" / invalid signature) is
|
|
2560
|
+
// the opposite case: it cites blocks that stay in this session's history,
|
|
2561
|
+
// so releasing the repair here makes the next turn replay the same blocks
|
|
2562
|
+
// and spend another rejected round trip on every turn that follows. That
|
|
2563
|
+
// repair has to outlive the stream it fixed.
|
|
2564
|
+
if (
|
|
2565
|
+
providerSessionState &&
|
|
2566
|
+
!thinkingReplayRepairPersistent &&
|
|
2567
|
+
(thinkingReplayRepairScope !== "none" || thinkingReplayRepairAttempts > 0)
|
|
2568
|
+
) {
|
|
2569
|
+
providerSessionState.thinkingReplayRepairScope = "none";
|
|
2570
|
+
providerSessionState.thinkingReplayRepairAttempts = 0;
|
|
2571
|
+
}
|
|
2572
|
+
// Release a recorded CPA steering once any stream completes: a
|
|
2573
|
+
// successful stream consumed it, and a failed one ends the turn (a
|
|
2574
|
+
// later turn's different user prompt would expire it anyway).
|
|
2575
|
+
if (providerSessionState?.cpaToolAliasSteering) {
|
|
2576
|
+
providerSessionState.cpaToolAliasSteering = undefined;
|
|
2577
|
+
}
|
|
2578
|
+
break;
|
|
2579
|
+
} catch (streamError) {
|
|
2580
|
+
const localAbortReason = activeAbortTracker.getLocalAbortReason();
|
|
2581
|
+
// Normalize unknown rejections (a primitive string from an injected
|
|
2582
|
+
// custom client is a supported surface) to a mutable Error. Boxed
|
|
2583
|
+
// primitives silently discard every fact stamped below, which let
|
|
2584
|
+
// a ceiling-bound upload slip past the one-attempt ceiling and
|
|
2585
|
+
// string-matched corrective branches re-upload the body.
|
|
2586
|
+
const streamFailure = localAbortReason ?? normalizeStreamFailure(streamError);
|
|
2587
|
+
attachAnthropicGraceFailureFacts(streamFailure, {
|
|
2588
|
+
elapsedMs: Date.now() - (firstEventWaitStartedAt ?? Date.now()),
|
|
2589
|
+
requestBytes,
|
|
2590
|
+
firstEventTimeoutMs,
|
|
2591
|
+
endpointClass,
|
|
2592
|
+
awaitingFirstEvent: !sawFirstSemanticEvent,
|
|
2593
|
+
});
|
|
2594
|
+
// A ceiling-bound upload failed before stream iteration began (for
|
|
2595
|
+
// example an immediate 529 from withResponse()): the grace clock
|
|
2596
|
+
// never started, so the facts above cannot apply, but the one-attempt
|
|
2597
|
+
// upload ceiling must still bound the outer provider retry loop.
|
|
2598
|
+
// Otherwise the multi-megabyte body is re-uploaded up to the default
|
|
2599
|
+
// streamMaxRetries budget despite the ceiling. Once iteration has
|
|
2600
|
+
// begun, only the grace-clock path above decides.
|
|
2601
|
+
if (requestUploadCeilingBound && firstEventWaitStartedAt === undefined) {
|
|
2602
|
+
Object.assign(streamFailure as Error, {
|
|
2603
|
+
requestBytes,
|
|
2604
|
+
endpointClass,
|
|
2605
|
+
retryMaxAttempts: ANTHROPIC_LARGE_FIRST_EVENT_TIMEOUT_MAX_ATTEMPTS,
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
const firstEventRetryMaxAttempts =
|
|
2609
|
+
typeof (streamFailure as { retryMaxAttempts?: unknown }).retryMaxAttempts === "number"
|
|
2610
|
+
? (streamFailure as { retryMaxAttempts: number }).retryMaxAttempts
|
|
2611
|
+
: undefined;
|
|
2612
|
+
if (localAbortReason || sawProviderSafetyStop) {
|
|
2613
|
+
throw streamFailure;
|
|
2614
|
+
}
|
|
2615
|
+
if (firstEventRetryMaxAttempts !== undefined && providerRetryAttempt + 1 >= firstEventRetryMaxAttempts) {
|
|
2616
|
+
throw streamFailure;
|
|
2617
|
+
}
|
|
2618
|
+
if (
|
|
2619
|
+
!options?.fallbackManaged &&
|
|
2620
|
+
!options?.disableProviderRetries &&
|
|
2621
|
+
!disableStrictTools &&
|
|
2622
|
+
firstTokenTime === undefined &&
|
|
2623
|
+
hasStrictAnthropicTools(params) &&
|
|
2624
|
+
isAnthropicStrictGrammarTooLargeError(streamFailure)
|
|
2625
|
+
) {
|
|
2626
|
+
strictFallbackErrorMessage = await finalizeErrorMessage(streamFailure, rawRequestDump);
|
|
2627
|
+
if (providerSessionState) {
|
|
2628
|
+
providerSessionState.strictToolsDisabled = true;
|
|
2629
|
+
}
|
|
2630
|
+
disableStrictTools = true;
|
|
2631
|
+
params = await prepareParams();
|
|
2632
|
+
providerRetryAttempt = 0;
|
|
2633
|
+
providerUploadCount++;
|
|
2634
|
+
resetOutputForRetry();
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (
|
|
2638
|
+
!droppedForcedToolChoice &&
|
|
2639
|
+
firstTokenTime === undefined &&
|
|
2640
|
+
!options?.fallbackManaged &&
|
|
2641
|
+
!options?.disableProviderRetries &&
|
|
2642
|
+
isSentForcedAnthropicToolChoice(params.tool_choice) &&
|
|
2643
|
+
isForcedToolChoiceUnsupportedError(streamFailure, true)
|
|
2644
|
+
) {
|
|
2645
|
+
const message = await finalizeErrorMessage(streamFailure, rawRequestDump);
|
|
2646
|
+
logger.debug("anthropic: forced tool_choice unsupported, retrying with auto tool choice", {
|
|
2647
|
+
model: model.id,
|
|
2648
|
+
error: message,
|
|
2649
|
+
});
|
|
2650
|
+
markToolChoiceIncapability(model, "auto", message);
|
|
2651
|
+
stream.push({
|
|
2652
|
+
type: "toolChoiceIncapability",
|
|
2653
|
+
api: output.api,
|
|
2654
|
+
provider: model.provider,
|
|
2655
|
+
model: model.id,
|
|
2656
|
+
requestedLevel: resolveToolChoice(model, options?.toolChoice).requestedLevel,
|
|
2657
|
+
resolvedLevel: "auto",
|
|
2658
|
+
reason: message,
|
|
2659
|
+
registryKey: resolveToolChoice(model, options?.toolChoice).registryKey,
|
|
2660
|
+
});
|
|
2661
|
+
droppedForcedToolChoice = true;
|
|
2662
|
+
params = await prepareParams();
|
|
2663
|
+
providerRetryAttempt = 0;
|
|
2664
|
+
providerUploadCount++;
|
|
2665
|
+
resetOutputForRetry();
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
const thinkingSignatureInvalid = isAnthropicThinkingSignatureInvalidError(streamFailure);
|
|
2669
|
+
const thinkingBlocksImmutable = isAnthropicThinkingBlockMutationError(streamFailure);
|
|
2670
|
+
const maskedProxyRejection = isAnthropicMaskedProxyRejection(streamFailure);
|
|
2671
|
+
if (
|
|
2672
|
+
!options?.fallbackManaged &&
|
|
2673
|
+
!options?.disableProviderRetries &&
|
|
2674
|
+
thinkingReplayRepairScope === "none" &&
|
|
2675
|
+
thinkingReplayRepairAttempts < ANTHROPIC_MAX_THINKING_REPAIRS &&
|
|
2676
|
+
firstTokenTime === undefined &&
|
|
2677
|
+
(thinkingSignatureInvalid ||
|
|
2678
|
+
thinkingBlocksImmutable ||
|
|
2679
|
+
// Masked proxy rejection: unclassifiable on its own, so the replayed
|
|
2680
|
+
// request shape is the evidence. Without signed thinking blocks in
|
|
2681
|
+
// flight there is nothing to repair and the error must surface.
|
|
2682
|
+
(maskedProxyRejection && hasNativeThinkingBlocks(params.messages)))
|
|
2683
|
+
) {
|
|
2684
|
+
const rejectedFingerprint = fingerprintAnthropicPayload(params);
|
|
2685
|
+
const scopes: Array<Exclude<AnthropicThinkingReplayRepairScope, "none">> = thinkingSignatureInvalid
|
|
2686
|
+
? ["all"]
|
|
2687
|
+
: ["latest", "all"];
|
|
2688
|
+
let candidate: AnthropicThinkingRepairCandidate | undefined;
|
|
2689
|
+
const transforms: Record<string, unknown> = {};
|
|
2690
|
+
for (const scope of scopes) {
|
|
2691
|
+
thinkingReplayRepairScope = scope;
|
|
2692
|
+
const candidateParams = await prepareParams();
|
|
2693
|
+
const fingerprint = fingerprintAnthropicPayload(candidateParams);
|
|
2694
|
+
const changed = anthropicPayloadChanged(rejectedFingerprint, fingerprint);
|
|
2695
|
+
transforms[scope] = { changed, sha256: fingerprint.sha256, bytes: fingerprint.bytes };
|
|
2696
|
+
if (changed) {
|
|
2697
|
+
candidate = { scope, params: candidateParams, fingerprint };
|
|
2698
|
+
break;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
if (rawRequestDump) {
|
|
2702
|
+
rawRequestDump.diagnostics = {
|
|
2703
|
+
...(rawRequestDump.diagnostics ?? {}),
|
|
2704
|
+
anthropicThinkingRepair: {
|
|
2705
|
+
rejected: rejectedFingerprint,
|
|
2706
|
+
disposition: candidate ? `send-${candidate.scope}` : "no-op-terminal",
|
|
2707
|
+
transforms,
|
|
2708
|
+
outgoingMismatch: describeAnthropicOutgoingPath(streamFailure, params),
|
|
2709
|
+
},
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
const captured = await finalizeAnthropicErrorMessage(streamFailure, rawRequestDump);
|
|
2713
|
+
logger.warn("anthropic: thinking replay rejected; evaluated bounded repair", {
|
|
2714
|
+
model: model.id,
|
|
2715
|
+
disposition: candidate ? `send-${candidate.scope}` : "no-op-terminal",
|
|
2716
|
+
rejectedSha256: rejectedFingerprint.sha256,
|
|
2717
|
+
rejectedBytes: rejectedFingerprint.bytes,
|
|
2718
|
+
diagnostic: captured,
|
|
2719
|
+
});
|
|
2720
|
+
if (!candidate) {
|
|
2721
|
+
thinkingReplayRepairScope = "none";
|
|
2722
|
+
throw createAnthropicThinkingRepairNoopError(streamFailure, params, rejectedFingerprint, captured);
|
|
2723
|
+
}
|
|
2724
|
+
const nextScope = candidate.scope;
|
|
2725
|
+
thinkingReplayRepairAttempts++;
|
|
2726
|
+
logger.debug("anthropic: repairing assistant thinking replay after provider rejection", {
|
|
2727
|
+
model: model.id,
|
|
2728
|
+
scope: nextScope,
|
|
2729
|
+
attempt: thinkingReplayRepairAttempts,
|
|
2730
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2731
|
+
});
|
|
2732
|
+
thinkingReplayRepairScope = nextScope;
|
|
2733
|
+
// Anything but the masked probe is caused by blocks that remain in
|
|
2734
|
+
// history, so this repair must survive the stream it is about to fix.
|
|
2735
|
+
if (!maskedProxyRejection) thinkingReplayRepairPersistent = true;
|
|
2736
|
+
if (providerSessionState) {
|
|
2737
|
+
providerSessionState.thinkingReplayRepairAttempts = thinkingReplayRepairAttempts;
|
|
2738
|
+
if (!maskedProxyRejection) {
|
|
2739
|
+
providerSessionState.thinkingReplayRepairScope = nextScope;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
params = candidate.params;
|
|
2743
|
+
// The corrective replay uploads the repaired body: count it so the
|
|
2744
|
+
// first-event timeout ceiling bounds TOTAL uploads (issue #4464).
|
|
2745
|
+
providerUploadCount++;
|
|
2746
|
+
// The provider retry budget is deliberately NOT reset here: a repair that
|
|
2747
|
+
// keeps being rejected must run out instead of renewing the budget it is
|
|
2748
|
+
// supposed to consume (issue #4011).
|
|
2749
|
+
resetOutputForRetry();
|
|
2750
|
+
continue;
|
|
2751
|
+
}
|
|
2752
|
+
// Managed attempts never take the repair branch above: the fallback
|
|
2753
|
+
// controller owns retries, so the provider must not retry inside the
|
|
2754
|
+
// attempt it was handed. That left the repair unreachable for the
|
|
2755
|
+
// coding agent, which prompts exclusively through managed attempts —
|
|
2756
|
+
// every turn rebuilt the same replay from the same history, drew the
|
|
2757
|
+
// same deterministic 400, and the session never converged (issue
|
|
2758
|
+
// #4262: one rejected 1.3 MB request every 12s, indefinitely).
|
|
2759
|
+
// Recording the escalation costs no round trip and keeps the retry
|
|
2760
|
+
// boundary intact: the next managed attempt builds a repaired replay.
|
|
2761
|
+
// The masked `api_error` stays out — it names no cause and may be a
|
|
2762
|
+
// transient blip, so only a rejection that provably indicts the
|
|
2763
|
+
// replayed thinking blocks may cost the session its native replay.
|
|
2764
|
+
if (
|
|
2765
|
+
options?.fallbackManaged &&
|
|
2766
|
+
providerSessionState &&
|
|
2767
|
+
providerSessionState.thinkingReplayRepairScope !== "all" &&
|
|
2768
|
+
firstTokenTime === undefined &&
|
|
2769
|
+
(thinkingSignatureInvalid || thinkingBlocksImmutable) &&
|
|
2770
|
+
hasNativeThinkingBlocks(params.messages)
|
|
2771
|
+
) {
|
|
2772
|
+
const rejectedFingerprint = fingerprintAnthropicPayload(params);
|
|
2773
|
+
thinkingReplayRepairScope = "all";
|
|
2774
|
+
const candidateParams = await prepareParams();
|
|
2775
|
+
const candidateFingerprint = fingerprintAnthropicPayload(candidateParams);
|
|
2776
|
+
const changed = anthropicPayloadChanged(rejectedFingerprint, candidateFingerprint);
|
|
2777
|
+
if (rawRequestDump) {
|
|
2778
|
+
rawRequestDump.diagnostics = {
|
|
2779
|
+
...(rawRequestDump.diagnostics ?? {}),
|
|
2780
|
+
anthropicThinkingRepair: {
|
|
2781
|
+
rejected: rejectedFingerprint,
|
|
2782
|
+
disposition: changed ? "record-all-for-managed-retry" : "no-op-terminal",
|
|
2783
|
+
transforms: {
|
|
2784
|
+
all: { changed, sha256: candidateFingerprint.sha256, bytes: candidateFingerprint.bytes },
|
|
2785
|
+
},
|
|
2786
|
+
outgoingMismatch: describeAnthropicOutgoingPath(streamFailure, params),
|
|
2787
|
+
},
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
const captured = await finalizeAnthropicErrorMessage(streamFailure, rawRequestDump);
|
|
2791
|
+
logger.warn("anthropic: managed thinking replay rejected; evaluated repair", {
|
|
2792
|
+
model: model.id,
|
|
2793
|
+
disposition: changed ? "record-all-for-managed-retry" : "no-op-terminal",
|
|
2794
|
+
rejectedSha256: rejectedFingerprint.sha256,
|
|
2795
|
+
rejectedBytes: rejectedFingerprint.bytes,
|
|
2796
|
+
diagnostic: captured,
|
|
2797
|
+
});
|
|
2798
|
+
if (!changed) {
|
|
2799
|
+
thinkingReplayRepairScope = "none";
|
|
2800
|
+
throw createAnthropicThinkingRepairNoopError(streamFailure, params, rejectedFingerprint, captured);
|
|
2801
|
+
}
|
|
2802
|
+
logger.debug("anthropic: recording thinking replay repair for the next managed attempt", {
|
|
2803
|
+
model: model.id,
|
|
2804
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2805
|
+
});
|
|
2806
|
+
providerSessionState.thinkingReplayRepairScope = "all";
|
|
2807
|
+
providerSessionState.thinkingReplayRejectedPayload = rejectedFingerprint;
|
|
2808
|
+
}
|
|
2809
|
+
if (
|
|
2810
|
+
!options?.fallbackManaged &&
|
|
2811
|
+
!options?.disableProviderRetries &&
|
|
2812
|
+
!dropFastMode &&
|
|
2813
|
+
resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
|
|
2814
|
+
firstTokenTime === undefined &&
|
|
2815
|
+
isAnthropicFastModeUnsupportedError(streamFailure)
|
|
2816
|
+
) {
|
|
2817
|
+
logger.debug("anthropic: fast mode unsupported, retrying without speed", {
|
|
2818
|
+
model: model.id,
|
|
2819
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2820
|
+
});
|
|
2821
|
+
if (providerSessionState) {
|
|
2822
|
+
providerSessionState.fastModeDisabled = true;
|
|
2823
|
+
}
|
|
2824
|
+
dropFastMode = true;
|
|
2825
|
+
params = await prepareParams();
|
|
2826
|
+
providerRetryAttempt = 0;
|
|
2827
|
+
providerUploadCount++;
|
|
2828
|
+
resetOutputForRetry();
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
if (
|
|
2832
|
+
!options?.fallbackManaged &&
|
|
2833
|
+
!options?.disableProviderRetries &&
|
|
2834
|
+
generatedCacheBudget > 0 &&
|
|
2835
|
+
firstTokenTime === undefined &&
|
|
2836
|
+
isAnthropicCacheBreakpointOverflowError(streamFailure)
|
|
2837
|
+
) {
|
|
2838
|
+
// The gateway's own markers already fill Anthropic's four slots, so
|
|
2839
|
+
// one of ours is the fifth. We cannot see the others, which makes the
|
|
2840
|
+
// rejection the only usable signal — and it says "too many", not
|
|
2841
|
+
// "none allowed". So give up one breakpoint at a time instead of all
|
|
2842
|
+
// caching at once: an endpoint that leaves a single slot free keeps
|
|
2843
|
+
// caching the conversation prefix, which is the marker that matters.
|
|
2844
|
+
const nextBudget: GeneratedCacheBudget = generatedCacheBudget === 2 ? 1 : 0;
|
|
2845
|
+
logger.debug("anthropic: cache breakpoint limit exceeded, reducing generated breakpoints", {
|
|
2846
|
+
model: model.id,
|
|
2847
|
+
from: generatedCacheBudget,
|
|
2848
|
+
to: nextBudget,
|
|
2849
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2850
|
+
});
|
|
2851
|
+
if (providerSessionState) {
|
|
2852
|
+
providerSessionState.generatedCacheBudget = nextBudget;
|
|
2853
|
+
}
|
|
2854
|
+
generatedCacheBudget = nextBudget;
|
|
2855
|
+
params = await prepareParams();
|
|
2856
|
+
providerRetryAttempt = 0;
|
|
2857
|
+
providerUploadCount++;
|
|
2858
|
+
resetOutputForRetry();
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
// CPA (Claude-OAuth proxy) alias-restore failure (issue #4338): the
|
|
2862
|
+
// proxy 500s the whole stream because the model emitted a cloaked
|
|
2863
|
+
// `mcp__<server>__<token>_<base>` tool name whose random token segment
|
|
2864
|
+
// matches nothing in the request. The generic 5xx retry below would
|
|
2865
|
+
// blindly re-send the unchanged request and re-sample the same drift;
|
|
2866
|
+
// instead, correct the request exactly once and terminalize on
|
|
2867
|
+
// recurrence. The narrow CPA phrase is the route gate, so native
|
|
2868
|
+
// Anthropic and non-CPA proxies are untouched.
|
|
2869
|
+
const cpaAliasFailure = parseCpaToolAliasRestoreFailure(streamFailure);
|
|
2870
|
+
if (cpaAliasFailure && firstTokenTime === undefined) {
|
|
2871
|
+
if (options?.fallbackManaged || options?.disableProviderRetries) {
|
|
2872
|
+
// The managed fallback controller owns retries: never retry
|
|
2873
|
+
// inside the attempt it handed us. Record the corrective
|
|
2874
|
+
// steering against this exact turn and surface the raw error;
|
|
2875
|
+
// the controller's next attempt rebuilds the request with the
|
|
2876
|
+
// steering. Without shared session state there is nowhere to
|
|
2877
|
+
// record, so fall through to the controller unchanged.
|
|
2878
|
+
if (!providerSessionState) throw streamFailure;
|
|
2879
|
+
const turnFingerprint = cpaTurnFingerprint(context.messages);
|
|
2880
|
+
if (providerSessionState.cpaToolAliasSteering?.turnFingerprint === turnFingerprint) {
|
|
2881
|
+
// The steering was already applied to this attempt and the proxy
|
|
2882
|
+
// rejected again: the single corrective attempt is spent. Surface
|
|
2883
|
+
// an actionable terminal error instead of another unchanged
|
|
2884
|
+
// resend.
|
|
2885
|
+
throw createCpaToolAliasTerminalError(
|
|
2886
|
+
cpaAliasFailure,
|
|
2887
|
+
resolveCpaCallableToolName(params, cpaAliasFailure),
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
providerSessionState.cpaToolAliasSteering = {
|
|
2891
|
+
message: buildCpaToolAliasSteering(
|
|
2892
|
+
cpaAliasFailure,
|
|
2893
|
+
resolveCpaCallableToolName(params, cpaAliasFailure),
|
|
2894
|
+
),
|
|
2895
|
+
turnFingerprint,
|
|
2896
|
+
};
|
|
2897
|
+
logger.debug("anthropic: recording CPA tool alias steering for the next managed attempt", {
|
|
2898
|
+
model: model.id,
|
|
2899
|
+
alias: cpaAliasFailure.alias,
|
|
2900
|
+
baseName: cpaAliasFailure.baseName,
|
|
2901
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2902
|
+
});
|
|
2903
|
+
throw streamFailure;
|
|
2904
|
+
}
|
|
2905
|
+
if (!cpaAliasRepairApplied) {
|
|
2906
|
+
cpaAliasRepairApplied = true;
|
|
2907
|
+
logger.debug("anthropic: repairing CPA tool alias restore failure with corrective steering", {
|
|
2908
|
+
model: model.id,
|
|
2909
|
+
alias: cpaAliasFailure.alias,
|
|
2910
|
+
baseName: cpaAliasFailure.baseName,
|
|
2911
|
+
error: streamFailure instanceof Error ? streamFailure.message : String(streamFailure),
|
|
2912
|
+
});
|
|
2913
|
+
appendCpaSteeringToMessages(
|
|
2914
|
+
params,
|
|
2915
|
+
buildCpaToolAliasSteering(cpaAliasFailure, resolveCpaCallableToolName(params, cpaAliasFailure)),
|
|
2916
|
+
);
|
|
2917
|
+
// Exactly one corrective attempt per request: the provider retry
|
|
2918
|
+
// budget is deliberately NOT reset, so a persistent failure runs
|
|
2919
|
+
// out instead of renewing the budget it is supposed to consume
|
|
2920
|
+
// (issue #4011), and the recurrence branch below terminalizes
|
|
2921
|
+
// before the generic 5xx retry can re-send the unchanged request.
|
|
2922
|
+
// This corrective replay uploads the steered body: count it so the
|
|
2923
|
+
// first-event timeout ceiling bounds TOTAL uploads (issue #4464).
|
|
2924
|
+
providerUploadCount++;
|
|
2925
|
+
resetOutputForRetry();
|
|
2926
|
+
continue;
|
|
2927
|
+
}
|
|
2928
|
+
throw createCpaToolAliasTerminalError(
|
|
2929
|
+
cpaAliasFailure,
|
|
2930
|
+
resolveCpaCallableToolName(params, cpaAliasFailure),
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
const isTransientEnvelopeFailure =
|
|
2934
|
+
isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
|
|
2935
|
+
const canRetryTransientEnvelopeFailure = isTransientEnvelopeFailure && !streamedReplayUnsafeContent;
|
|
2936
|
+
const canRetryProviderFailure =
|
|
2937
|
+
firstTokenTime === undefined && isProviderRetryableError(streamFailure, model.provider);
|
|
2938
|
+
if (
|
|
2939
|
+
activeAbortTracker.wasCallerAbort() ||
|
|
2940
|
+
(firstEventRetryMaxAttempts !== undefined &&
|
|
2941
|
+
providerRetryAttempt + 1 >= firstEventRetryMaxAttempts) ||
|
|
2942
|
+
providerRetryAttempt >= resolveRetryBudget(options?.streamMaxRetries, PROVIDER_MAX_RETRIES) ||
|
|
2943
|
+
(!canRetryTransientEnvelopeFailure && !canRetryProviderFailure)
|
|
2944
|
+
) {
|
|
2945
|
+
throw streamFailure;
|
|
2946
|
+
}
|
|
2947
|
+
providerRetryAttempt++;
|
|
2948
|
+
providerUploadCount++;
|
|
2949
|
+
const delayMs = PROVIDER_BASE_DELAY_MS * 2 ** (providerRetryAttempt - 1);
|
|
2950
|
+
if (options?.providerRetryWait) {
|
|
2951
|
+
await options.providerRetryWait(delayMs, options.signal);
|
|
2952
|
+
} else {
|
|
2953
|
+
await scheduler.wait(delayMs, { signal: options?.signal });
|
|
2954
|
+
}
|
|
2955
|
+
resetOutputForRetry();
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
for (const block of blocksByAnthropicIndex.values()) {
|
|
2960
|
+
delete (block as { index?: number }).index;
|
|
2961
|
+
if (block.type === "toolCall") {
|
|
2962
|
+
truncatedToolCalls.add(block);
|
|
2963
|
+
block.incompleteArguments = true;
|
|
2964
|
+
block.incompleteArgumentsReason = "truncated";
|
|
2965
|
+
if (block.partialJson.trim()) {
|
|
2966
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
2967
|
+
captureUnicodeEscapeEvidence(block, block.partialJson);
|
|
2968
|
+
}
|
|
2969
|
+
delete (block as { partialJson?: string }).partialJson;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
blocksByAnthropicIndex.clear();
|
|
2973
|
+
for (const block of output.content) {
|
|
2974
|
+
if (block.type === "toolCall" && truncatedToolCalls.has(block)) {
|
|
2975
|
+
block.incompleteArguments = true;
|
|
2976
|
+
block.incompleteArgumentsReason = "truncated";
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
output.duration = Date.now() - startTime;
|
|
2980
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
2981
|
+
if (dropFastMode && resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
|
|
2982
|
+
output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
|
|
2983
|
+
}
|
|
2984
|
+
// Defense-in-depth (#4443): when the provider stream assembles an
|
|
2985
|
+
// assistant message whose content carries directly adjacent private
|
|
2986
|
+
// blocks, emit a bounded diagnostic naming only the envelope shape —
|
|
2987
|
+
// block count and adjacent-pair presence — never raw thinking text,
|
|
2988
|
+
// signatures, or redacted payloads. The send-boundary collapse
|
|
2989
|
+
// remains the wire source of truth; this is a read-only observation.
|
|
2990
|
+
// Scoped to this stream invocation: each completed turn with the
|
|
2991
|
+
// defect is a distinct upstream producer worth surfacing, so the
|
|
2992
|
+
// diagnostic is not latched across invocations.
|
|
2993
|
+
if (hasAdjacentPrivateThinkingBlocks(output.content)) {
|
|
2994
|
+
logger.warn("anthropic: stream assembled assistant content with adjacent thinking blocks", {
|
|
2995
|
+
model: model.id,
|
|
2996
|
+
provider: model.provider,
|
|
2997
|
+
contentBlockCount: output.content.length,
|
|
2998
|
+
hasAdjacentPrivateBlocks: true,
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
3002
|
+
stream.end();
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
for (const block of output.content) {
|
|
3005
|
+
delete (block as { index?: number }).index;
|
|
3006
|
+
delete (block as { partialJson?: string }).partialJson;
|
|
3007
|
+
}
|
|
3008
|
+
const localAbortReason = activeAbortTracker.getLocalAbortReason();
|
|
3009
|
+
output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
|
|
3010
|
+
output.errorStatus = extractHttpStatusFromError(localAbortReason ?? error);
|
|
3011
|
+
output.transportFailure = transportFailureFacts(localAbortReason ?? error) ?? output.transportFailure;
|
|
3012
|
+
if (output.errorKind !== "provider_safety_stop" || !output.errorMessage) {
|
|
3013
|
+
output.errorMessage =
|
|
3014
|
+
localAbortReason?.message ?? (await finalizeAnthropicErrorMessage(error, rawRequestDump));
|
|
3015
|
+
}
|
|
3016
|
+
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
3017
|
+
output.duration = Date.now() - startTime;
|
|
3018
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
3019
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
3020
|
+
stream.end();
|
|
3021
|
+
}
|
|
3022
|
+
})();
|
|
3023
|
+
|
|
3024
|
+
return stream;
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
export type AnthropicSystemBlock = {
|
|
3028
|
+
type: "text";
|
|
3029
|
+
text: string;
|
|
3030
|
+
cache_control?: AnthropicCacheControl;
|
|
3031
|
+
};
|
|
3032
|
+
type SystemBlockOptions = {
|
|
3033
|
+
includeClaudeCodeInstruction?: boolean;
|
|
3034
|
+
extraInstructions?: string[];
|
|
3035
|
+
billingPayload?: unknown;
|
|
3036
|
+
cacheControl?: AnthropicCacheControl;
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
export function buildAnthropicSystemBlocks(
|
|
3040
|
+
systemPrompt: readonly string[] | undefined,
|
|
3041
|
+
options: SystemBlockOptions = {},
|
|
3042
|
+
): AnthropicSystemBlock[] | undefined {
|
|
3043
|
+
const { includeClaudeCodeInstruction = false, extraInstructions = [], billingPayload, cacheControl } = options;
|
|
3044
|
+
const blocks: AnthropicSystemBlock[] = [];
|
|
3045
|
+
const sanitizedPrompts = normalizeSystemPrompts(systemPrompt);
|
|
3046
|
+
const trimmedInstructions = extraInstructions.map(instruction => instruction.trim()).filter(Boolean);
|
|
3047
|
+
const hasBillingHeader = sanitizedPrompts.some(prompt => prompt.includes(CLAUDE_BILLING_HEADER_PREFIX));
|
|
3048
|
+
|
|
3049
|
+
if (includeClaudeCodeInstruction && !hasBillingHeader) {
|
|
3050
|
+
const payloadSeed = billingPayload ?? {
|
|
3051
|
+
system: sanitizedPrompts,
|
|
3052
|
+
extraInstructions: trimmedInstructions,
|
|
3053
|
+
};
|
|
3054
|
+
blocks.push(
|
|
3055
|
+
{ type: "text", text: createClaudeBillingHeader(payloadSeed) },
|
|
3056
|
+
{
|
|
3057
|
+
type: "text",
|
|
3058
|
+
text: claudeCodeSystemInstruction,
|
|
3059
|
+
},
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
for (const instruction of trimmedInstructions) {
|
|
3064
|
+
blocks.push({ type: "text", text: instruction });
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
for (const systemPrompt of sanitizedPrompts) {
|
|
3068
|
+
blocks.push({ type: "text", text: systemPrompt });
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// Attach cache_control to the LAST emitted block only. Anthropic breakpoints are cumulative
|
|
3072
|
+
// prefix cuts, so a single trailing breakpoint covers every preceding block; spreading
|
|
3073
|
+
// cache_control across N blocks wastes slots against the 4-breakpoint cap.
|
|
3074
|
+
const lastIndex = blocks.length - 1;
|
|
3075
|
+
if (cacheControl && lastIndex >= 0) {
|
|
3076
|
+
blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
return blocks.length > 0 ? blocks : undefined;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
export function normalizeExtraBetas(betas?: string[] | string): string[] {
|
|
3083
|
+
if (!betas) return [];
|
|
3084
|
+
const raw = Array.isArray(betas) ? betas : betas.split(",");
|
|
3085
|
+
return raw.map(beta => beta.trim()).filter(beta => beta.length > 0);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): AnthropicClientOptionsResult {
|
|
3089
|
+
const {
|
|
3090
|
+
model,
|
|
3091
|
+
apiKey,
|
|
3092
|
+
extraBetas = [],
|
|
3093
|
+
stream = true,
|
|
3094
|
+
interleavedThinking = true,
|
|
3095
|
+
headers,
|
|
3096
|
+
dynamicHeaders,
|
|
3097
|
+
hasTools = false,
|
|
3098
|
+
isOAuth,
|
|
3099
|
+
onSseEvent,
|
|
3100
|
+
} = args;
|
|
3101
|
+
const compat = getAnthropicCompat(model);
|
|
3102
|
+
const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinkingDisplay(model.id);
|
|
3103
|
+
const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
|
|
3104
|
+
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
3105
|
+
const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
|
|
3106
|
+
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
3107
|
+
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
3108
|
+
const baseFetch = args.fetch ?? fetch;
|
|
3109
|
+
const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
|
|
3110
|
+
const debugFetch = onSseEvent ? wrapFetchForSseDebug(boundedFetch, event => onSseEvent(event, model)) : boundedFetch;
|
|
3111
|
+
// Bound the connect/headers phase. The first-event watchdog arms only after
|
|
3112
|
+
// response headers arrive, so a request whose connection dies before headers
|
|
3113
|
+
// was previously governed only by the Anthropic SDK's 10-minute default per
|
|
3114
|
+
// attempt times its internal retry budget — observable as an endless spinner
|
|
3115
|
+
// right after a completed tool call.
|
|
3116
|
+
const sdkTimeoutMs = resolveAnthropicSdkRequestTimeoutMs(
|
|
3117
|
+
model.provider,
|
|
3118
|
+
args.streamFirstEventTimeoutMs,
|
|
3119
|
+
args.streamIdleTimeoutMs,
|
|
3120
|
+
);
|
|
3121
|
+
if (model.provider === "github-copilot") {
|
|
3122
|
+
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
3123
|
+
const betaFeatures = [...extraBetas];
|
|
3124
|
+
if (needsFineGrainedToolStreamingBeta) {
|
|
3125
|
+
betaFeatures.push(fineGrainedToolStreamingBeta);
|
|
3126
|
+
}
|
|
3127
|
+
const defaultHeaders = mergeHeaders(
|
|
3128
|
+
{
|
|
3129
|
+
Accept: stream ? "text/event-stream" : "application/json",
|
|
3130
|
+
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
3131
|
+
Authorization: `Bearer ${copilotApiKey}`,
|
|
3132
|
+
...(betaFeatures.length > 0 ? { "anthropic-beta": buildBetaHeader([], betaFeatures) } : {}),
|
|
3133
|
+
},
|
|
3134
|
+
model.headers,
|
|
3135
|
+
dynamicHeaders,
|
|
3136
|
+
headers,
|
|
3137
|
+
);
|
|
3138
|
+
|
|
3139
|
+
return {
|
|
3140
|
+
isOAuthToken: false,
|
|
3141
|
+
apiKey: null,
|
|
3142
|
+
authToken: copilotApiKey,
|
|
3143
|
+
baseURL: baseUrl,
|
|
3144
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
3145
|
+
...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
|
|
3146
|
+
dangerouslyAllowBrowser: true,
|
|
3147
|
+
defaultHeaders,
|
|
3148
|
+
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
3149
|
+
fetch: debugFetch,
|
|
3150
|
+
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
const betaFeatures = [...extraBetas];
|
|
3155
|
+
if (needsFineGrainedToolStreamingBeta) {
|
|
3156
|
+
betaFeatures.push(fineGrainedToolStreamingBeta);
|
|
3157
|
+
}
|
|
3158
|
+
if (needsInterleavedBeta) {
|
|
3159
|
+
betaFeatures.push(interleavedThinkingBeta);
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const defaultHeaders = buildAnthropicHeaders({
|
|
3163
|
+
apiKey,
|
|
3164
|
+
baseUrl,
|
|
3165
|
+
isOAuth: oauthToken,
|
|
3166
|
+
extraBetas: betaFeatures,
|
|
3167
|
+
stream,
|
|
3168
|
+
modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
|
|
3169
|
+
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
|
3170
|
+
zcodeSourceHeaders: model.provider === "glm-zcode",
|
|
3171
|
+
});
|
|
3172
|
+
|
|
3173
|
+
if (model.provider === "cloudflare-ai-gateway") {
|
|
3174
|
+
return {
|
|
3175
|
+
isOAuthToken: false,
|
|
3176
|
+
apiKey: null,
|
|
3177
|
+
authToken: null,
|
|
3178
|
+
baseURL: baseUrl,
|
|
3179
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
3180
|
+
...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
|
|
3181
|
+
dangerouslyAllowBrowser: true,
|
|
3182
|
+
defaultHeaders,
|
|
3183
|
+
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
3184
|
+
fetch: debugFetch,
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// JetBrains AI (Ingrazzio) authenticates with a plain `Authorization: Bearer`
|
|
3189
|
+
// token and rejects requests that also carry `X-Api-Key`. `buildAnthropicHeaders`
|
|
3190
|
+
// already emits the bearer for non-Anthropic hosts, so keep the SDK from adding
|
|
3191
|
+
// its own API-key header on top of it.
|
|
3192
|
+
if (model.provider === "jetbrains-junie") {
|
|
3193
|
+
return {
|
|
3194
|
+
isOAuthToken: false,
|
|
3195
|
+
apiKey: null,
|
|
3196
|
+
authToken: null,
|
|
3197
|
+
baseURL: baseUrl,
|
|
3198
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
3199
|
+
...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
|
|
3200
|
+
dangerouslyAllowBrowser: true,
|
|
3201
|
+
defaultHeaders,
|
|
3202
|
+
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
3203
|
+
fetch: debugFetch,
|
|
3204
|
+
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
return {
|
|
3209
|
+
isOAuthToken: oauthToken,
|
|
3210
|
+
apiKey: oauthToken ? null : apiKey,
|
|
3211
|
+
authToken: oauthToken ? apiKey : undefined,
|
|
3212
|
+
baseURL: baseUrl,
|
|
3213
|
+
maxRetries: resolveRetryBudget(args.requestMaxRetries, 5),
|
|
3214
|
+
...(sdkTimeoutMs !== undefined ? { timeout: sdkTimeoutMs } : {}),
|
|
3215
|
+
dangerouslyAllowBrowser: true,
|
|
3216
|
+
defaultHeaders,
|
|
3217
|
+
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
3218
|
+
fetch: debugFetch,
|
|
3219
|
+
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function createClient(
|
|
3224
|
+
model: Model<"anthropic-messages">,
|
|
3225
|
+
args: AnthropicClientOptionsArgs,
|
|
3226
|
+
): { client: Anthropic; isOAuthToken: boolean } {
|
|
3227
|
+
const { isOAuthToken: oauthToken, ...clientOptions } = buildAnthropicClientOptions({ ...args, model });
|
|
3228
|
+
const client = new Anthropic(clientOptions);
|
|
3229
|
+
return { client, isOAuthToken: oauthToken };
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/**
|
|
3233
|
+
* Anthropic rejects extended thinking combined with a forced tool choice, so such a
|
|
3234
|
+
* request drops `thinking`/`output_config`. Reports whether the forced-choice branch
|
|
3235
|
+
* applied so the caller can keep the replayed history consistent with it.
|
|
3236
|
+
*/
|
|
3237
|
+
function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming): boolean {
|
|
3238
|
+
const toolChoice = params.tool_choice;
|
|
3239
|
+
if (!toolChoice) return false;
|
|
3240
|
+
if (toolChoice.type !== "any" && toolChoice.type !== "tool") return false;
|
|
3241
|
+
delete params.thinking;
|
|
3242
|
+
delete params.output_config;
|
|
3243
|
+
return true;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
function hasNativeThinkingBlocks(messages: MessageParam[]): boolean {
|
|
3247
|
+
return messages.some(
|
|
3248
|
+
message =>
|
|
3249
|
+
Array.isArray(message.content) &&
|
|
3250
|
+
message.content.some(block => block.type === "thinking" || block.type === "redacted_thinking"),
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
/**
|
|
3255
|
+
* Would the latest assistant turn lose a thinking block on its way to the wire?
|
|
3256
|
+
*
|
|
3257
|
+
* `convertAnthropicMessages` can only replay a `thinking` block natively when it
|
|
3258
|
+
* still carries the bytes Anthropic signed. A block that arrived as a bare
|
|
3259
|
+
* start/stop pair — no `thinking_delta`, no `signature_delta` — has neither, so
|
|
3260
|
+
* it is silently dropped, and Anthropic rejects the turn it produced for coming
|
|
3261
|
+
* back without it. Same for a `redactedThinking` block whose opaque payload is
|
|
3262
|
+
* gone. Only the latest assistant message is inspected because that is the turn
|
|
3263
|
+
* Anthropic validates against its own output.
|
|
3264
|
+
*/
|
|
3265
|
+
function latestAssistantThinkingIsUnreplayable(messages: Message[], model: Model<"anthropic-messages">): boolean {
|
|
3266
|
+
const index = messages.findLastIndex(message => message.role === "assistant");
|
|
3267
|
+
if (index < 0) return false;
|
|
3268
|
+
|
|
3269
|
+
const assistant = messages[index] as AssistantMessage;
|
|
3270
|
+
// Cross-API history degrades to text rather than replaying native blocks, so
|
|
3271
|
+
// nothing is lost and nothing needs repairing.
|
|
3272
|
+
if (assistant.api !== "anthropic-messages") return false;
|
|
3273
|
+
// Endpoints that never sign thinking replay unsigned blocks verbatim.
|
|
3274
|
+
const requiresSignature = !isNonSigningAnthropicEndpoint(model);
|
|
3275
|
+
|
|
3276
|
+
return assistant.content.some(block => {
|
|
3277
|
+
if (block.type === "redactedThinking") return block.data.trim().length === 0;
|
|
3278
|
+
if (block.type !== "thinking") return false;
|
|
3279
|
+
// A block with empty text and no signature cannot go back on the wire:
|
|
3280
|
+
// `convertAnthropicMessages` drops it, and Anthropic rejects the turn for
|
|
3281
|
+
// arriving without it. A block with a valid signature AND non-empty text is
|
|
3282
|
+
// replayable. But a signed block whose text was emptied — e.g. by
|
|
3283
|
+
// clear_thinking_20251015 — carries a stale signature that signing endpoints
|
|
3284
|
+
// reject on replay (issue #4247). Non-signing endpoints replay unsigned
|
|
3285
|
+
// blocks verbatim, so only they treat a missing signature as unreplayable.
|
|
3286
|
+
const hasSignature = !!block.thinkingSignature?.trim();
|
|
3287
|
+
const isEmpty = !block.thinking.trim();
|
|
3288
|
+
if (!hasSignature) return requiresSignature;
|
|
3289
|
+
if (isEmpty && requiresSignature) return true;
|
|
3290
|
+
return false;
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function mapAnthropicToolChoice(
|
|
3295
|
+
toolChoice: NonNullable<ResolveToolChoiceResult["resolvedChoice"]>,
|
|
3296
|
+
isOAuthToken: boolean,
|
|
3297
|
+
): NonNullable<MessageCreateParamsStreaming["tool_choice"]> | undefined {
|
|
3298
|
+
if (typeof toolChoice === "string") {
|
|
3299
|
+
if (toolChoice === "required") return { type: "any" };
|
|
3300
|
+
return { type: toolChoice };
|
|
3301
|
+
}
|
|
3302
|
+
if ("function" in toolChoice) {
|
|
3303
|
+
const name = typeof toolChoice.function === "string" ? toolChoice.function : toolChoice.function.name;
|
|
3304
|
+
return { type: "tool", name: isOAuthToken ? applyClaudeToolPrefix(name) : name };
|
|
3305
|
+
}
|
|
3306
|
+
if ("name" in toolChoice && typeof toolChoice.name === "string") {
|
|
3307
|
+
return {
|
|
3308
|
+
...toolChoice,
|
|
3309
|
+
type: "tool",
|
|
3310
|
+
name: isOAuthToken ? applyClaudeToolPrefix(toolChoice.name) : toolChoice.name,
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
return toolChoice as NonNullable<MessageCreateParamsStreaming["tool_choice"]>;
|
|
3314
|
+
}
|
|
3315
|
+
function isSentForcedAnthropicToolChoice(toolChoice: MessageCreateParamsStreaming["tool_choice"] | undefined): boolean {
|
|
3316
|
+
return toolChoice?.type === "any" || toolChoice?.type === "tool";
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
|
|
3320
|
+
const thinking = params.thinking;
|
|
3321
|
+
if (thinking?.type !== "enabled") return;
|
|
3322
|
+
|
|
3323
|
+
const budgetTokens = thinking.budget_tokens ?? 0;
|
|
3324
|
+
if (budgetTokens <= 0) return;
|
|
3325
|
+
|
|
3326
|
+
const maxTokens = params.max_tokens ?? 0;
|
|
3327
|
+
const requiredMaxTokens = budgetTokens + OUTPUT_FALLBACK_BUFFER;
|
|
3328
|
+
if (maxTokens < requiredMaxTokens) {
|
|
3329
|
+
const modelMaxTokens =
|
|
3330
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0
|
|
3331
|
+
? model.maxTokens
|
|
3332
|
+
: Math.max(maxTokens, requiredMaxTokens);
|
|
3333
|
+
params.max_tokens = Math.min(requiredMaxTokens, modelMaxTokens);
|
|
3334
|
+
}
|
|
3335
|
+
// Anthropic requires budget_tokens strictly below max_tokens; when the cap
|
|
3336
|
+
// cannot fit the requested budget plus the output buffer, shrink the budget
|
|
3337
|
+
// (or disable thinking) instead of sending an invalid pair.
|
|
3338
|
+
const cappedBudget = params.max_tokens - OUTPUT_FALLBACK_BUFFER;
|
|
3339
|
+
if (cappedBudget < budgetTokens) {
|
|
3340
|
+
if (cappedBudget <= 0) {
|
|
3341
|
+
params.thinking = { type: "disabled" };
|
|
3342
|
+
} else {
|
|
3343
|
+
params.thinking = { type: "enabled", budget_tokens: cappedBudget };
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
type CacheControlBlock = {
|
|
3349
|
+
cache_control?: AnthropicCacheControl | null;
|
|
3350
|
+
};
|
|
3351
|
+
|
|
3352
|
+
type AnthropicCacheParams = MessageCreateParamsStreaming & {
|
|
3353
|
+
cache_control?: AnthropicCacheControl;
|
|
3354
|
+
};
|
|
3355
|
+
|
|
3356
|
+
type AnthropicCacheMode = "automatic" | "explicit" | "none";
|
|
3357
|
+
|
|
3358
|
+
function isCacheableContentBlock(block: ContentBlockParam): boolean {
|
|
3359
|
+
if (block.type === "thinking" || block.type === "redacted_thinking") return false;
|
|
3360
|
+
return block.type !== "text" || block.text.trim().length > 0;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function cacheControlError(path: string, reason: string): Error {
|
|
3364
|
+
return new Error(`Invalid Anthropic cache_control at ${path}: ${reason}`);
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function validateCacheControl(control: unknown, path: string, seenFiveMinute: { value: boolean }): void {
|
|
3368
|
+
if (!isRecord(control) || control.type !== "ephemeral") {
|
|
3369
|
+
throw cacheControlError(path, 'expected { type: "ephemeral" }');
|
|
3370
|
+
}
|
|
3371
|
+
if (control.ttl !== undefined && control.ttl !== "5m" && control.ttl !== "1h") {
|
|
3372
|
+
throw cacheControlError(path, 'ttl must be "5m" or "1h"');
|
|
3373
|
+
}
|
|
3374
|
+
if (control.ttl === "1h") {
|
|
3375
|
+
if (seenFiveMinute.value) throw cacheControlError(path, "1h TTL must precede 5m TTL");
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
seenFiveMinute.value = true;
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function validateCacheControls(params: AnthropicCacheParams): void {
|
|
3382
|
+
const seenFiveMinute = { value: false };
|
|
3383
|
+
let count = 0;
|
|
3384
|
+
const validate = (control: unknown, path: string): void => {
|
|
3385
|
+
if (control == null) return;
|
|
3386
|
+
count++;
|
|
3387
|
+
validateCacheControl(control, path, seenFiveMinute);
|
|
3388
|
+
};
|
|
3389
|
+
if (!Array.isArray(params.messages)) throw cacheControlError("messages", "must be an array");
|
|
3390
|
+
|
|
3391
|
+
validate(params.cache_control, "cache_control");
|
|
3392
|
+
for (const [index, tool] of (params.tools ?? []).entries()) {
|
|
3393
|
+
validate((tool as CacheControlBlock).cache_control, `tools[${index}].cache_control`);
|
|
3394
|
+
}
|
|
3395
|
+
if (Array.isArray(params.system)) {
|
|
3396
|
+
for (const [index, block] of params.system.entries()) {
|
|
3397
|
+
validate((block as CacheControlBlock).cache_control, `system[${index}].cache_control`);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
for (const [messageIndex, message] of params.messages.entries()) {
|
|
3401
|
+
if (!Array.isArray(message.content)) continue;
|
|
3402
|
+
for (const [blockIndex, block] of message.content.entries()) {
|
|
3403
|
+
const control = (block as CacheControlBlock).cache_control;
|
|
3404
|
+
if (control != null && !isCacheableContentBlock(block)) {
|
|
3405
|
+
throw cacheControlError(
|
|
3406
|
+
`messages[${messageIndex}].content[${blockIndex}].cache_control`,
|
|
3407
|
+
"block is not cacheable",
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
validate(control, `messages[${messageIndex}].content[${blockIndex}].cache_control`);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
if (count > 4) throw cacheControlError("cache_control", "at most four total breakpoints are allowed");
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function applyCacheControlToLastCacheableBlock(
|
|
3417
|
+
blocks: Array<ContentBlockParam & CacheControlBlock>,
|
|
3418
|
+
cacheControl: AnthropicCacheControl,
|
|
3419
|
+
): boolean {
|
|
3420
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
3421
|
+
const block = blocks[index];
|
|
3422
|
+
if (!isCacheableContentBlock(block)) continue;
|
|
3423
|
+
blocks[index] = { ...block, cache_control: { ...cacheControl } };
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
function isHumanUserMessage(message: MessageCreateParamsStreaming["messages"][number]): boolean {
|
|
3430
|
+
if (message.role !== "user") return false;
|
|
3431
|
+
if (typeof message.content === "string") return true;
|
|
3432
|
+
return message.content.some(block => block.type !== "tool_result");
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
function applyExplicitPromptCaching(
|
|
3436
|
+
params: AnthropicCacheParams,
|
|
3437
|
+
cacheControl: AnthropicCacheControl,
|
|
3438
|
+
budget: GeneratedCacheBudget,
|
|
3439
|
+
): void {
|
|
3440
|
+
if (budget === 0) return;
|
|
3441
|
+
if (countCacheControlBreakpoints(params) >= 4) return;
|
|
3442
|
+
|
|
3443
|
+
const currentUserIndex = params.messages.findLastIndex(isHumanUserMessage);
|
|
3444
|
+
if (currentUserIndex < 0) return;
|
|
3445
|
+
const currentUser = params.messages[currentUserIndex];
|
|
3446
|
+
if (!currentUser) return;
|
|
3447
|
+
|
|
3448
|
+
// Tool results are encoded as role "user" on the wire but belong to the
|
|
3449
|
+
// assistant tool-use turn immediately before them. Anchor the latest completed
|
|
3450
|
+
// assistant turn so the reusable prefix advances during an agent tool loop,
|
|
3451
|
+
// while keeping the newest tool output outside the cache boundary.
|
|
3452
|
+
//
|
|
3453
|
+
// This anchor is the higher-value marker of the two: it covers the whole
|
|
3454
|
+
// conversation prefix, so a reduced budget is spent here first. It only
|
|
3455
|
+
// consumes budget when a marker is actually placed — on a first turn there is
|
|
3456
|
+
// no assistant message yet, and the reduced budget must still reach the
|
|
3457
|
+
// current-turn marker below rather than emitting nothing at all.
|
|
3458
|
+
let remaining: number = budget;
|
|
3459
|
+
for (let index = params.messages.length - 1; index >= 0; index--) {
|
|
3460
|
+
const message = params.messages[index];
|
|
3461
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
3462
|
+
if (
|
|
3463
|
+
applyCacheControlToLastCacheableBlock(
|
|
3464
|
+
message.content as Array<ContentBlockParam & CacheControlBlock>,
|
|
3465
|
+
cacheControl,
|
|
3466
|
+
)
|
|
3467
|
+
) {
|
|
3468
|
+
remaining -= 1;
|
|
3469
|
+
break;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (remaining < 1) return;
|
|
3474
|
+
if (countCacheControlBreakpoints(params) >= 4) return;
|
|
3475
|
+
if (typeof currentUser.content === "string" && currentUser.content.trim()) {
|
|
3476
|
+
currentUser.content = [{ type: "text", text: currentUser.content, cache_control: { ...cacheControl } }];
|
|
3477
|
+
} else if (Array.isArray(currentUser.content)) {
|
|
3478
|
+
applyCacheControlToLastCacheableBlock(
|
|
3479
|
+
currentUser.content as Array<ContentBlockParam & CacheControlBlock>,
|
|
3480
|
+
cacheControl,
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function applyPromptCaching(
|
|
3486
|
+
params: AnthropicCacheParams,
|
|
3487
|
+
cacheMode: AnthropicCacheMode,
|
|
3488
|
+
cacheControl?: AnthropicCacheControl,
|
|
3489
|
+
budget: GeneratedCacheBudget = 2,
|
|
3490
|
+
): void {
|
|
3491
|
+
if (!cacheControl || cacheMode === "none" || budget === 0) return;
|
|
3492
|
+
validateCacheControls(params);
|
|
3493
|
+
if (cacheMode === "automatic") {
|
|
3494
|
+
// Automatic mode only ever emits one marker, so any non-zero budget
|
|
3495
|
+
// covers it; the zero case already returned above.
|
|
3496
|
+
params.cache_control = { ...cacheControl };
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
applyExplicitPromptCaching(params, cacheControl, budget);
|
|
3500
|
+
validateCacheControls(params);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
export function normalizeCacheControlTtlOrdering(params: MessageCreateParamsStreaming): void {
|
|
3504
|
+
validateCacheControls(params as AnthropicCacheParams);
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
function countCacheControlBreakpoints(params: AnthropicCacheParams): number {
|
|
3508
|
+
let total = params.cache_control ? 1 : 0;
|
|
3509
|
+
for (const tool of params.tools ?? []) if ((tool as CacheControlBlock).cache_control) total++;
|
|
3510
|
+
if (Array.isArray(params.system)) {
|
|
3511
|
+
for (const block of params.system) if ((block as CacheControlBlock).cache_control) total++;
|
|
3512
|
+
}
|
|
3513
|
+
for (const message of params.messages) {
|
|
3514
|
+
if (!Array.isArray(message.content)) continue;
|
|
3515
|
+
for (const block of message.content) if ((block as CacheControlBlock).cache_control) total++;
|
|
3516
|
+
}
|
|
3517
|
+
return total;
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
function enforceCacheControlLimit(params: MessageCreateParamsStreaming, maxBreakpoints: number): void {
|
|
3521
|
+
if (maxBreakpoints !== 4) throw new Error("Anthropic supports exactly four cache breakpoints");
|
|
3522
|
+
validateCacheControls(params as AnthropicCacheParams);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
function buildParams(
|
|
3526
|
+
model: Model<"anthropic-messages">,
|
|
3527
|
+
baseUrl: string,
|
|
3528
|
+
context: Context,
|
|
3529
|
+
isOAuthToken: boolean,
|
|
3530
|
+
options?: AnthropicOptions,
|
|
3531
|
+
disableStrictTools = false,
|
|
3532
|
+
thinkingRepair?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
|
|
3533
|
+
generatedCacheBudget: GeneratedCacheBudget = 2,
|
|
3534
|
+
): MessageCreateParamsStreaming {
|
|
3535
|
+
const { mode: cacheMode, cacheControl } = getCacheControl(
|
|
3536
|
+
model,
|
|
3537
|
+
baseUrl,
|
|
3538
|
+
options?.cacheRetention,
|
|
3539
|
+
generatedCacheBudget,
|
|
3540
|
+
);
|
|
3541
|
+
|
|
3542
|
+
const params: AnthropicSamplingParams = {
|
|
3543
|
+
model: model.id,
|
|
3544
|
+
messages: convertAnthropicMessages(context.messages, model, isOAuthToken, thinkingRepair),
|
|
3545
|
+
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
3546
|
+
stream: true,
|
|
3547
|
+
};
|
|
3548
|
+
if (options?.temperature !== undefined && !options?.thinkingEnabled) {
|
|
3549
|
+
params.temperature = options.temperature;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
if (options?.topP !== undefined) {
|
|
3553
|
+
params.top_p = options.topP;
|
|
3554
|
+
}
|
|
3555
|
+
if (options?.topK !== undefined) {
|
|
3556
|
+
params.top_k = options.topK;
|
|
3557
|
+
}
|
|
3558
|
+
if (options?.stopSequences?.length) {
|
|
3559
|
+
const seqs = options.stopSequences;
|
|
3560
|
+
if (seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX && !warnedStopSequencesTrim) {
|
|
3561
|
+
warnedStopSequencesTrim = true;
|
|
3562
|
+
logger.warn("anthropic: stop_sequences exceeds 4; extra entries dropped", {
|
|
3563
|
+
received: seqs.length,
|
|
3564
|
+
kept: ANTHROPIC_STOP_SEQUENCES_MAX,
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
params.stop_sequences =
|
|
3568
|
+
seqs.length > ANTHROPIC_STOP_SEQUENCES_MAX ? seqs.slice(0, ANTHROPIC_STOP_SEQUENCES_MAX) : seqs;
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
// Opus 4.7+ rejects non-default sampling parameters with 400 error.
|
|
3572
|
+
if (hasOpus47ApiRestrictions(model.id)) {
|
|
3573
|
+
delete params.top_p;
|
|
3574
|
+
delete params.top_k;
|
|
3575
|
+
delete params.temperature;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
if (context.tools) {
|
|
3579
|
+
params.tools = convertTools(
|
|
3580
|
+
context.tools,
|
|
3581
|
+
isOAuthToken,
|
|
3582
|
+
// The Claude Code OAuth surface mishandles `strict: true` tools:
|
|
3583
|
+
// streamed tool_use blocks arrive with empty/undefined arguments and
|
|
3584
|
+
// occasionally corrupted names (works with PI_NO_STRICT=1). Never
|
|
3585
|
+
// request strict tool use on OAuth requests.
|
|
3586
|
+
disableStrictTools || isOAuthToken || model.provider === "github-copilot",
|
|
3587
|
+
getAnthropicCompat(model).supportsEagerToolInputStreaming,
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
if (model.reasoning) {
|
|
3592
|
+
if (options?.thinkingEnabled) {
|
|
3593
|
+
const mode = model.thinking?.mode;
|
|
3594
|
+
const requestedEffort = options.reasoning;
|
|
3595
|
+
const effort =
|
|
3596
|
+
options.effort ??
|
|
3597
|
+
(requestedEffort ? mapEffortToAnthropicAdaptiveEffort(model, requestedEffort) : undefined);
|
|
3598
|
+
|
|
3599
|
+
const compat = getAnthropicCompat(model);
|
|
3600
|
+
if (mode === "anthropic-adaptive" && !compat.disableAdaptiveThinking) {
|
|
3601
|
+
// Starting with Anthropic model Opus 4.7, adaptive thinking content is omitted from the
|
|
3602
|
+
// response by default. Opt into summarized reasoning so thinking deltas keep
|
|
3603
|
+
// streaming with human-readable content for callers that rely on it.
|
|
3604
|
+
const adaptive: { type: "adaptive"; display?: AnthropicThinkingDisplay } = { type: "adaptive" };
|
|
3605
|
+
if (supportsAdaptiveThinkingDisplay(model.id)) {
|
|
3606
|
+
adaptive.display = options.thinkingDisplay ?? "summarized";
|
|
3607
|
+
}
|
|
3608
|
+
params.thinking = adaptive as typeof params.thinking;
|
|
3609
|
+
if (effort) {
|
|
3610
|
+
// SDK OutputConfig.effort typings may lag Anthropic's adaptive effort literals.
|
|
3611
|
+
// Cast so newly supported levels can pass through before the SDK catches up.
|
|
3612
|
+
params.output_config = { effort } as typeof params.output_config;
|
|
3613
|
+
}
|
|
3614
|
+
} else {
|
|
3615
|
+
params.thinking = {
|
|
3616
|
+
type: "enabled",
|
|
3617
|
+
budget_tokens: options.thinkingBudgetTokens || 1024,
|
|
3618
|
+
display: options.thinkingDisplay ?? "summarized",
|
|
3619
|
+
} as typeof params.thinking;
|
|
3620
|
+
if (mode === "anthropic-budget-effort" && effort) {
|
|
3621
|
+
params.output_config = { effort } as typeof params.output_config;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
const metadataUserId = resolveAnthropicMetadataUserId(options?.metadata?.user_id, isOAuthToken);
|
|
3628
|
+
if (metadataUserId) {
|
|
3629
|
+
params.metadata = { user_id: metadataUserId };
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
if (resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
|
|
3633
|
+
(params as ParamsWithSpeed).speed = "fast";
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
if (options?.toolChoice) {
|
|
3637
|
+
const resolution = resolveToolChoice(model, options.toolChoice);
|
|
3638
|
+
if (resolution.degraded && resolution.supportSource !== "runtime") {
|
|
3639
|
+
logger.debug("anthropic: degrading tool_choice for model capability", {
|
|
3640
|
+
model: model.id,
|
|
3641
|
+
requestedLevel: resolution.requestedLevel,
|
|
3642
|
+
resolvedLevel: resolution.resolvedLevel,
|
|
3643
|
+
reason: resolution.reason,
|
|
3644
|
+
supportSource: resolution.supportSource,
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
if (resolution.resolvedChoice) {
|
|
3648
|
+
const mappedToolChoice = mapAnthropicToolChoice(resolution.resolvedChoice, isOAuthToken);
|
|
3649
|
+
if (mappedToolChoice) {
|
|
3650
|
+
params.tool_choice = mappedToolChoice;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// A forced tool choice strips `thinking` from the request. Signed thinking blocks
|
|
3656
|
+
// replayed from history belong to a thinking-enabled request, and Anthropic rejects
|
|
3657
|
+
// that pair with `thinking`/`redacted_thinking` blocks "cannot be modified", so the
|
|
3658
|
+
// replay has to degrade in the same rebuild. Runs before the billing/system payload
|
|
3659
|
+
// snapshot so the attribution hash covers the messages actually sent.
|
|
3660
|
+
if (disableThinkingIfToolChoiceForced(params) && hasNativeThinkingBlocks(params.messages)) {
|
|
3661
|
+
params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
|
|
3662
|
+
...thinkingRepair,
|
|
3663
|
+
repairAllAssistantThinking: true,
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// Anthropic compares the latest assistant message against the turn it actually
|
|
3668
|
+
// produced, and rejects it when a `thinking`/`redacted_thinking` block that was
|
|
3669
|
+
// in that response is missing. A block Anthropic streamed as a start/stop pair
|
|
3670
|
+
// with no `thinking_delta` and no `signature_delta` lands in history empty and
|
|
3671
|
+
// unsigned, and `convertAnthropicMessages` then drops it: the turn goes back
|
|
3672
|
+
// carrying only its `tool_use`, and the request is rejected before a token
|
|
3673
|
+
// streams. The rejection is recoverable — the repair drops native thinking from
|
|
3674
|
+
// the whole replay — but only after a full round trip has been spent, and the
|
|
3675
|
+
// condition is visible locally, so detect it here and degrade in the first
|
|
3676
|
+
// build instead of paying for the 400 to discover it.
|
|
3677
|
+
if (
|
|
3678
|
+
!thinkingRepair?.repairAllAssistantThinking &&
|
|
3679
|
+
latestAssistantThinkingIsUnreplayable(context.messages, model) &&
|
|
3680
|
+
hasNativeThinkingBlocks(params.messages)
|
|
3681
|
+
) {
|
|
3682
|
+
params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
|
|
3683
|
+
...thinkingRepair,
|
|
3684
|
+
repairAllAssistantThinking: true,
|
|
3685
|
+
});
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
|
|
3689
|
+
const billingSystemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
3690
|
+
const billingPayload = shouldInjectClaudeCodeInstruction
|
|
3691
|
+
? {
|
|
3692
|
+
...params,
|
|
3693
|
+
...(billingSystemPrompts.length > 0 ? { system: billingSystemPrompts } : {}),
|
|
3694
|
+
}
|
|
3695
|
+
: undefined;
|
|
3696
|
+
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
3697
|
+
includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
|
|
3698
|
+
billingPayload,
|
|
3699
|
+
});
|
|
3700
|
+
if (systemBlocks) {
|
|
3701
|
+
params.system = systemBlocks;
|
|
3702
|
+
}
|
|
3703
|
+
ensureMaxTokensForThinking(params, model);
|
|
3704
|
+
applyPromptCaching(params as AnthropicCacheParams, cacheMode, cacheControl, generatedCacheBudget);
|
|
3705
|
+
enforceCacheControlLimit(params, 4);
|
|
3706
|
+
normalizeCacheControlTtlOrdering(params);
|
|
3707
|
+
|
|
3708
|
+
return params;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
/**
|
|
3712
|
+
* Z.AI's Anthropic-compatible proxy at `api.z.ai/api/anthropic` deserializes
|
|
3713
|
+
* tool_result blocks into a Python class that accesses `.id`, even though
|
|
3714
|
+
* Anthropic's standard tool_result schema only carries `tool_use_id`. Detect
|
|
3715
|
+
* that endpoint so we can emit the non-standard alias for it without
|
|
3716
|
+
* polluting requests to api.anthropic.com or other compatible proxies.
|
|
3717
|
+
* See: https://github.com/can1357/oh-my-pi/issues/814
|
|
3718
|
+
*/
|
|
3719
|
+
function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
3720
|
+
if (model.provider === "zai" || model.provider === "glm-zcode") return true;
|
|
3721
|
+
const baseUrl = model.baseUrl;
|
|
3722
|
+
if (!baseUrl) return false;
|
|
3723
|
+
try {
|
|
3724
|
+
return new URL(baseUrl).hostname.toLowerCase() === "api.z.ai";
|
|
3725
|
+
} catch {
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
/**
|
|
3731
|
+
* Returns true for providers whose Anthropic-compatible endpoints do NOT
|
|
3732
|
+
* implement signature-based thinking-chain integrity (DeepSeek, Z.AI, etc.).
|
|
3733
|
+
* For these providers, unsigned thinking blocks must be preserved as
|
|
3734
|
+
* `type: "thinking"` instead of being degraded to text.
|
|
3735
|
+
*/
|
|
3736
|
+
function isNonSigningAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
3737
|
+
// Known non-signing providers
|
|
3738
|
+
if (model.provider === "zai" || model.provider === "glm-zcode" || model.provider === "deepseek") return true;
|
|
3739
|
+
const baseUrl = model.baseUrl;
|
|
3740
|
+
if (!baseUrl) return false;
|
|
3741
|
+
try {
|
|
3742
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
3743
|
+
return hostname === "api.deepseek.com" || hostname.endsWith(".deepseek.com");
|
|
3744
|
+
} catch {
|
|
3745
|
+
return false;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResultMessage): ContentBlockParam {
|
|
3750
|
+
const block: ContentBlockParam = {
|
|
3751
|
+
type: "tool_result",
|
|
3752
|
+
tool_use_id: msg.toolCallId,
|
|
3753
|
+
content: convertContentBlocks(msg.content, model.input.includes("image")),
|
|
3754
|
+
is_error: msg.isError,
|
|
3755
|
+
};
|
|
3756
|
+
if (isZaiAnthropicEndpoint(model)) {
|
|
3757
|
+
// Z.AI workaround (issue #814): include `id` aliased to `tool_use_id`.
|
|
3758
|
+
(block as unknown as Record<string, unknown>).id = msg.toolCallId;
|
|
3759
|
+
}
|
|
3760
|
+
return block;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
/**
|
|
3764
|
+
* Anthropic rejects a replayed assistant message containing adjacent
|
|
3765
|
+
* `thinking`/`redacted_thinking` blocks — even when each block individually
|
|
3766
|
+
* carries a valid signature — with a 400 citing the second block: "cannot be
|
|
3767
|
+
* modified. These blocks must remain as in the original response." (issue #4416)
|
|
3768
|
+
*
|
|
3769
|
+
* The adjacency can originate from the provider stream (two `content_block_start`
|
|
3770
|
+
* events for `thinking` in one message with no intervening `tool_use`), from a
|
|
3771
|
+
* history mutation that removed a separating `tool_use`, or from an earlier
|
|
3772
|
+
* conversion phase in `convertAnthropicMessages` that skipped an empty `text`
|
|
3773
|
+
* block sitting between two thinking blocks. Because that last path exists, the
|
|
3774
|
+
* invariant cannot be enforced in the shared `transformMessages` phase — it must
|
|
3775
|
+
* run on the final wire output.
|
|
3776
|
+
*
|
|
3777
|
+
* This collapses each run of adjacent `thinking`/`redacted_thinking` blocks down
|
|
3778
|
+
* to the first block, preserving its bytes, signature, and type verbatim (never
|
|
3779
|
+
* concatenating, editing, synthesizing, or choosing the last). Blocks separated
|
|
3780
|
+
* by any non-thinking block (`text`, `tool_use`, …) are legitimate
|
|
3781
|
+
* interleaved-thinking shape and pass through unchanged. `thinking` and
|
|
3782
|
+
* `redacted_thinking` are treated as one adjacency class per the API contract.
|
|
3783
|
+
*
|
|
3784
|
+
* The pass is O(n) per message and idempotent: an already-collapsed array is a
|
|
3785
|
+
* no-op, so re-runs through `convertAnthropicMessages` (e.g. forced-tool-choice
|
|
3786
|
+
* or unreplayable-thinking rebuilds) are safe.
|
|
3787
|
+
*/
|
|
3788
|
+
function collapseAdjacentThinkingBlocks(messages: MessageParam[]): void {
|
|
3789
|
+
for (const message of messages) {
|
|
3790
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
3791
|
+
const content = message.content;
|
|
3792
|
+
let write = 0;
|
|
3793
|
+
let inThinkingRun = false;
|
|
3794
|
+
for (let read = 0; read < content.length; read++) {
|
|
3795
|
+
const block = content[read];
|
|
3796
|
+
if (block === undefined) continue;
|
|
3797
|
+
const isThinkingBlock = block.type === "thinking" || block.type === "redacted_thinking";
|
|
3798
|
+
if (isThinkingBlock && inThinkingRun) continue; // only the first block of a run survives
|
|
3799
|
+
inThinkingRun = isThinkingBlock;
|
|
3800
|
+
content[write++] = block;
|
|
3801
|
+
}
|
|
3802
|
+
if (write < content.length) content.length = write;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
export function convertAnthropicMessages(
|
|
3807
|
+
messages: Message[],
|
|
3808
|
+
model: Model<"anthropic-messages">,
|
|
3809
|
+
isOAuthToken: boolean,
|
|
3810
|
+
options?: { repairLatestAssistantThinking?: boolean; repairAllAssistantThinking?: boolean },
|
|
3811
|
+
): MessageParam[] {
|
|
3812
|
+
const params: MessageParam[] = [];
|
|
3813
|
+
|
|
3814
|
+
const transformedMessages = transformMessages(messages, model, normalizeToolCallId, options);
|
|
3815
|
+
|
|
3816
|
+
for (let i = 0; i < transformedMessages.length; i++) {
|
|
3817
|
+
const msg = transformedMessages[i];
|
|
3818
|
+
|
|
3819
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
3820
|
+
if (!msg.content) continue;
|
|
3821
|
+
|
|
3822
|
+
if (typeof msg.content === "string") {
|
|
3823
|
+
if (msg.content.trim().length > 0) {
|
|
3824
|
+
params.push({
|
|
3825
|
+
role: "user",
|
|
3826
|
+
content: msg.content.toWellFormed(),
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
} else {
|
|
3830
|
+
const contentBlocks = convertContentBlocks(msg.content, model.input.includes("image"));
|
|
3831
|
+
if (typeof contentBlocks === "string") {
|
|
3832
|
+
if (contentBlocks.trim().length === 0) continue;
|
|
3833
|
+
params.push({
|
|
3834
|
+
role: "user",
|
|
3835
|
+
content: contentBlocks,
|
|
3836
|
+
});
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
if (contentBlocks.length === 0) continue;
|
|
3840
|
+
params.push({
|
|
3841
|
+
role: "user",
|
|
3842
|
+
content: contentBlocks,
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
} else if (msg.role === "assistant") {
|
|
3846
|
+
const blocks: ContentBlockParam[] = [];
|
|
3847
|
+
const hasSignedThinking = msg.content.some(
|
|
3848
|
+
block =>
|
|
3849
|
+
block.type === "thinking" && !!block.thinkingSignature && block.thinkingSignature.trim().length > 0,
|
|
3850
|
+
);
|
|
3851
|
+
|
|
3852
|
+
for (const block of msg.content) {
|
|
3853
|
+
if (block.type === "text") {
|
|
3854
|
+
if (block.text.trim().length === 0) continue;
|
|
3855
|
+
blocks.push({
|
|
3856
|
+
type: "text",
|
|
3857
|
+
text: block.text.toWellFormed(),
|
|
3858
|
+
});
|
|
3859
|
+
} else if (block.type === "thinking") {
|
|
3860
|
+
if (hasSignedThinking) {
|
|
3861
|
+
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
3862
|
+
if (block.thinking.trim().length === 0) continue;
|
|
3863
|
+
blocks.push({
|
|
3864
|
+
type: "text",
|
|
3865
|
+
text: block.thinking.toWellFormed(),
|
|
3866
|
+
});
|
|
3867
|
+
continue;
|
|
3868
|
+
}
|
|
3869
|
+
blocks.push({
|
|
3870
|
+
type: "thinking",
|
|
3871
|
+
thinking: block.thinking,
|
|
3872
|
+
signature: block.thinkingSignature,
|
|
3873
|
+
});
|
|
3874
|
+
continue;
|
|
3875
|
+
}
|
|
3876
|
+
if (block.thinking.trim().length === 0) continue;
|
|
3877
|
+
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
3878
|
+
if (isNonSigningAnthropicEndpoint(model)) {
|
|
3879
|
+
blocks.push({
|
|
3880
|
+
type: "thinking",
|
|
3881
|
+
thinking: block.thinking.toWellFormed(),
|
|
3882
|
+
signature: "",
|
|
3883
|
+
});
|
|
3884
|
+
} else {
|
|
3885
|
+
blocks.push({
|
|
3886
|
+
type: "text",
|
|
3887
|
+
text: block.thinking.toWellFormed(),
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
} else {
|
|
3891
|
+
blocks.push({
|
|
3892
|
+
type: "thinking",
|
|
3893
|
+
thinking: block.thinking.toWellFormed(),
|
|
3894
|
+
signature: block.thinkingSignature,
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
} else if (block.type === "redactedThinking") {
|
|
3898
|
+
if (block.data.trim().length === 0) continue;
|
|
3899
|
+
blocks.push({
|
|
3900
|
+
type: "redacted_thinking",
|
|
3901
|
+
data: block.data,
|
|
3902
|
+
});
|
|
3903
|
+
} else if (block.type === "toolCall") {
|
|
3904
|
+
blocks.push({
|
|
3905
|
+
type: "tool_use",
|
|
3906
|
+
id: block.id,
|
|
3907
|
+
name: isOAuthToken ? applyClaudeToolPrefix(block.name) : block.name,
|
|
3908
|
+
input: sanitizeJsonStrings(block.arguments ?? {}),
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
if (blocks.length === 0) continue;
|
|
3913
|
+
params.push({
|
|
3914
|
+
role: "assistant",
|
|
3915
|
+
content: blocks,
|
|
3916
|
+
});
|
|
3917
|
+
} else if (msg.role === "toolResult") {
|
|
3918
|
+
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
|
|
3919
|
+
const toolResults: ContentBlockParam[] = [];
|
|
3920
|
+
|
|
3921
|
+
// Add the current tool result
|
|
3922
|
+
toolResults.push(buildToolResultBlock(model, msg));
|
|
3923
|
+
|
|
3924
|
+
// Look ahead for consecutive toolResult messages
|
|
3925
|
+
let j = i + 1;
|
|
3926
|
+
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
|
3927
|
+
const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
|
|
3928
|
+
toolResults.push(buildToolResultBlock(model, nextMsg));
|
|
3929
|
+
j++;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// Skip the messages we've already processed
|
|
3933
|
+
i = j - 1;
|
|
3934
|
+
|
|
3935
|
+
// Add a single user message with all tool results
|
|
3936
|
+
params.push({
|
|
3937
|
+
role: "user",
|
|
3938
|
+
content: toolResults,
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// Final send-time invariant (issue #4416): collapse any run of adjacent
|
|
3944
|
+
// `thinking`/`redacted_thinking` blocks within one assistant message down to
|
|
3945
|
+
// the first block. This runs on the wire output because earlier phases here
|
|
3946
|
+
// (e.g. skipping empty text blocks) can themselves create the adjacency.
|
|
3947
|
+
collapseAdjacentThinkingBlocks(params);
|
|
3948
|
+
if (params.length > 0 && params[params.length - 1]?.role === "assistant") {
|
|
3949
|
+
params.push({ role: "user", content: "Continue." });
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
return params;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
/**
|
|
3956
|
+
* JSON Schema whitelist for Anthropic tool `input_schema` nodes.
|
|
3957
|
+
*
|
|
3958
|
+
* Mirrors the Anthropic Python SDK's `lib/_parse/_transform.py::transform_schema`:
|
|
3959
|
+
* we keep only structural/metadata keywords Anthropic's validator honors, and demote
|
|
3960
|
+
* anything else into the node's `description` as `\n\n{key: value, ...}` so the model
|
|
3961
|
+
* still sees the constraint as a natural-language hint.
|
|
3962
|
+
*
|
|
3963
|
+
* `Set` (not `Record<string, true>`) because membership is probed against arbitrary
|
|
3964
|
+
* user/Zod-derived schema keys: a literal Record would falsely match prototype names
|
|
3965
|
+
* like `"toString"` and silently strip valid properties.
|
|
3966
|
+
*/
|
|
3967
|
+
const ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP = new Set([
|
|
3968
|
+
"$ref",
|
|
3969
|
+
"$defs",
|
|
3970
|
+
"$schema",
|
|
3971
|
+
"definitions",
|
|
3972
|
+
"type",
|
|
3973
|
+
"anyOf",
|
|
3974
|
+
"oneOf",
|
|
3975
|
+
"allOf",
|
|
3976
|
+
"enum",
|
|
3977
|
+
"const",
|
|
3978
|
+
"description",
|
|
3979
|
+
"title",
|
|
3980
|
+
"default",
|
|
3981
|
+
"nullable",
|
|
3982
|
+
]);
|
|
3983
|
+
/** Keys preserved on `type: "object"` nodes (in addition to the universal set). */
|
|
3984
|
+
const ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP = new Set(["properties", "required", "additionalProperties"]);
|
|
3985
|
+
/** Keys preserved on `type: "array"` nodes; `minItems` only when its value is 0 or 1. */
|
|
3986
|
+
const ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP = new Set(["items", "prefixItems", "minItems"]);
|
|
3987
|
+
/** Keys preserved on `type: "string"` nodes; `format` only when its value is in the supported list. */
|
|
3988
|
+
const ANTHROPIC_TOOL_SCHEMA_STRING_KEEP = new Set(["format"]);
|
|
3989
|
+
/**
|
|
3990
|
+
* String `format` values Anthropic accepts; everything else (including `pattern`-style
|
|
3991
|
+
* format hints) gets demoted into `description`. Matches `SupportedStringFormats` in the
|
|
3992
|
+
* Anthropic SDK's `_transform.py`.
|
|
3993
|
+
*/
|
|
3994
|
+
const ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS = new Set([
|
|
3995
|
+
"date-time",
|
|
3996
|
+
"time",
|
|
3997
|
+
"date",
|
|
3998
|
+
"duration",
|
|
3999
|
+
"email",
|
|
4000
|
+
"hostname",
|
|
4001
|
+
"uri",
|
|
4002
|
+
"ipv4",
|
|
4003
|
+
"ipv6",
|
|
4004
|
+
"uuid",
|
|
4005
|
+
]);
|
|
4006
|
+
const ANTHROPIC_STRICT_TOOL_ALLOWLIST = new Set(["bash", "python", "edit", "find"]);
|
|
4007
|
+
const MAX_ANTHROPIC_STRICT_TOOLS = 20;
|
|
4008
|
+
const MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS = 24;
|
|
4009
|
+
const MAX_ANTHROPIC_STRICT_UNION_PARAMETERS = 16;
|
|
4010
|
+
|
|
4011
|
+
/**
|
|
4012
|
+
* Pick the principal non-null scalar type from a `type` keyword. Anthropic accepts
|
|
4013
|
+
* `type` as either a single string or an array (e.g. `["number", "null"]` for a
|
|
4014
|
+
* nullable value); the SDK whitelist is keyed off the scalar type, with `"null"`
|
|
4015
|
+
* ignored so nullable variants are normalized as their underlying type.
|
|
4016
|
+
*/
|
|
4017
|
+
function pickAnthropicScalarType(type: unknown): string | undefined {
|
|
4018
|
+
if (typeof type === "string") return type;
|
|
4019
|
+
if (Array.isArray(type)) {
|
|
4020
|
+
for (const entry of type) {
|
|
4021
|
+
if (typeof entry === "string" && entry !== "null") return entry;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return undefined;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
function anthropicPerTypeKeep(scalarType: string | undefined): Set<string> | undefined {
|
|
4028
|
+
switch (scalarType) {
|
|
4029
|
+
case "object":
|
|
4030
|
+
return ANTHROPIC_TOOL_SCHEMA_OBJECT_KEEP;
|
|
4031
|
+
case "array":
|
|
4032
|
+
return ANTHROPIC_TOOL_SCHEMA_ARRAY_KEEP;
|
|
4033
|
+
case "string":
|
|
4034
|
+
return ANTHROPIC_TOOL_SCHEMA_STRING_KEEP;
|
|
4035
|
+
default:
|
|
4036
|
+
return undefined;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
/**
|
|
4041
|
+
* Per-schema-object memoization slot for the normalized Anthropic tool form. We stamp
|
|
4042
|
+
* the result onto the host via a `Symbol` property (mirroring `utils/schema/stamps.ts`)
|
|
4043
|
+
* instead of using a `WeakMap`: it's a single hidden-class slot, so warm reads are
|
|
4044
|
+
* direct property access and write-once cycles resolve to the in-progress result.
|
|
4045
|
+
*/
|
|
4046
|
+
const kAnthropicToolNormal = Symbol("pi.schema.anthropic.toolNormal");
|
|
4047
|
+
|
|
4048
|
+
/**
|
|
4049
|
+
* Normalize a JSON Schema node for Anthropic tool `input_schema`.
|
|
4050
|
+
*
|
|
4051
|
+
* Applies the full whitelist semantics from the Anthropic Python SDK's
|
|
4052
|
+
* `lib/_parse/_transform.py::transform_schema`:
|
|
4053
|
+
*
|
|
4054
|
+
* 1. Universal keys (`$ref`, `$defs`, `type`, `anyOf`/`oneOf`/`allOf`, `enum`, `const`,
|
|
4055
|
+
* `description`, `title`, `default`, `nullable`) are preserved on every node.
|
|
4056
|
+
* 2. Per-type keys are kept additively (object → `properties`/`required`/`additionalProperties`,
|
|
4057
|
+
* array → `items`/`prefixItems` plus `minItems` only when 0 or 1, string → `format`
|
|
4058
|
+
* only when in the supported value set).
|
|
4059
|
+
* 3. Everything else is demoted into the node's `description` as `\n\n{key: value, ...}`
|
|
4060
|
+
* so the model still sees the constraint as a natural-language hint.
|
|
4061
|
+
*
|
|
4062
|
+
* Object nodes default to `additionalProperties: false`, but explicit open-map
|
|
4063
|
+
* declarations (`additionalProperties: true` or a schema literal — Zod's
|
|
4064
|
+
* `z.record(z.string(), z.unknown())` produces `{}`) are preserved. The strict-mode
|
|
4065
|
+
* pass downstream demotes those shapes to non-strict instead of fabricating a closed
|
|
4066
|
+
* object, so callers like the resolve tool keep working open-map semantics.
|
|
4067
|
+
*/
|
|
4068
|
+
export function normalizeAnthropicToolSchema(schema: unknown): unknown {
|
|
4069
|
+
if (Array.isArray(schema)) return schema.map(entry => normalizeAnthropicToolSchema(entry));
|
|
4070
|
+
if (!isRecord(schema)) return schema;
|
|
4071
|
+
|
|
4072
|
+
const slot = schema as Record<symbol, Record<string, unknown> | undefined>;
|
|
4073
|
+
const existing = slot[kAnthropicToolNormal];
|
|
4074
|
+
if (existing !== undefined) return existing;
|
|
4075
|
+
|
|
4076
|
+
const result: Record<string, unknown> = {};
|
|
4077
|
+
// Pre-stamp before recursion so cyclic schemas resolve to the in-progress object
|
|
4078
|
+
// (mirrors the WeakMap-set-before-recurse pattern the original implementation used).
|
|
4079
|
+
Object.defineProperty(schema, kAnthropicToolNormal, { value: result, writable: true, configurable: true });
|
|
4080
|
+
|
|
4081
|
+
const scalarType = pickAnthropicScalarType(schema.type);
|
|
4082
|
+
const perTypeKeep = anthropicPerTypeKeep(scalarType);
|
|
4083
|
+
const spill: Array<[string, unknown]> = [];
|
|
4084
|
+
|
|
4085
|
+
for (const key in schema) {
|
|
4086
|
+
if (!Object.hasOwn(schema, key)) continue;
|
|
4087
|
+
const value = schema[key];
|
|
4088
|
+
if (ANTHROPIC_TOOL_SCHEMA_UNIVERSAL_KEEP.has(key) || perTypeKeep?.has(key)) {
|
|
4089
|
+
result[key] = value;
|
|
4090
|
+
} else {
|
|
4091
|
+
spill.push([key, value]);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// Per-type conditional keys: prune within the kept set.
|
|
4096
|
+
if (scalarType === "string") {
|
|
4097
|
+
const format = result.format;
|
|
4098
|
+
if (typeof format === "string" && !ANTHROPIC_TOOL_SCHEMA_STRING_FORMATS.has(format)) {
|
|
4099
|
+
spill.push(["format", format]);
|
|
4100
|
+
delete result.format;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
if (scalarType === "array" && result.minItems !== undefined) {
|
|
4104
|
+
const minItems = result.minItems;
|
|
4105
|
+
if (!(typeof minItems === "number" && (minItems === 0 || minItems === 1))) {
|
|
4106
|
+
spill.push(["minItems", minItems]);
|
|
4107
|
+
delete result.minItems;
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
if (scalarType === "object" && result.additionalProperties === undefined) {
|
|
4111
|
+
result.additionalProperties = false;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// Recurse on structural keys.
|
|
4115
|
+
if (isRecord(result.properties)) {
|
|
4116
|
+
const normalizedProperties: Record<string, unknown> = {};
|
|
4117
|
+
const sourceProperties = result.properties as Record<string, unknown>;
|
|
4118
|
+
for (const propName in sourceProperties) {
|
|
4119
|
+
if (!Object.hasOwn(sourceProperties, propName)) continue;
|
|
4120
|
+
normalizedProperties[propName] = normalizeAnthropicToolSchema(sourceProperties[propName]);
|
|
4121
|
+
}
|
|
4122
|
+
result.properties = normalizedProperties;
|
|
4123
|
+
}
|
|
4124
|
+
if (isRecord(result.additionalProperties)) {
|
|
4125
|
+
const normalized = normalizeAnthropicToolSchema(result.additionalProperties);
|
|
4126
|
+
if (isRecord(normalized) && Object.keys(normalized).length === 0) {
|
|
4127
|
+
result.additionalProperties = true;
|
|
4128
|
+
} else {
|
|
4129
|
+
result.additionalProperties = normalized;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
if (Array.isArray(result.items)) {
|
|
4133
|
+
result.items = result.items.map(item => normalizeAnthropicToolSchema(item));
|
|
4134
|
+
} else if (isRecord(result.items)) {
|
|
4135
|
+
result.items = normalizeAnthropicToolSchema(result.items);
|
|
4136
|
+
}
|
|
4137
|
+
if (Array.isArray(result.prefixItems)) {
|
|
4138
|
+
result.prefixItems = result.prefixItems.map(item => normalizeAnthropicToolSchema(item));
|
|
4139
|
+
}
|
|
4140
|
+
for (const key of COMBINATOR_KEYS) {
|
|
4141
|
+
const variants = result[key];
|
|
4142
|
+
if (Array.isArray(variants)) {
|
|
4143
|
+
result[key] = variants.map(variant => normalizeAnthropicToolSchema(variant));
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
for (const defsKey of ["$defs", "definitions"] as const) {
|
|
4147
|
+
const definitions = result[defsKey];
|
|
4148
|
+
if (!isRecord(definitions)) continue;
|
|
4149
|
+
const normalizedDefs: Record<string, unknown> = {};
|
|
4150
|
+
const sourceDefs = definitions as Record<string, unknown>;
|
|
4151
|
+
for (const name in sourceDefs) {
|
|
4152
|
+
if (!Object.hasOwn(sourceDefs, name)) continue;
|
|
4153
|
+
normalizedDefs[name] = normalizeAnthropicToolSchema(sourceDefs[name]);
|
|
4154
|
+
}
|
|
4155
|
+
result[defsKey] = normalizedDefs;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
spillToDescription(result, spill);
|
|
4159
|
+
return result;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
type AnthropicToolInputSchema = Anthropic.Messages.Tool["input_schema"];
|
|
4163
|
+
|
|
4164
|
+
type AnthropicToolSchemaPlan = {
|
|
4165
|
+
inputSchema: AnthropicToolInputSchema;
|
|
4166
|
+
strict: boolean;
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4169
|
+
type AnthropicStrictBudget = {
|
|
4170
|
+
optionalRemaining: number;
|
|
4171
|
+
unionRemaining: number;
|
|
4172
|
+
optionalCount: number;
|
|
4173
|
+
unionCount: number;
|
|
4174
|
+
};
|
|
4175
|
+
|
|
4176
|
+
function hasAnthropicUnionType(schema: Record<string, unknown>): boolean {
|
|
4177
|
+
return Array.isArray(schema.type) || Array.isArray(schema.anyOf);
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
function hasNullVariant(schema: Record<string, unknown>): boolean {
|
|
4181
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
4182
|
+
return Array.isArray(schema.anyOf) && schema.anyOf.some(variant => isRecord(variant) && variant.type === "null");
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
function makeAnthropicNullableSchema(schema: unknown, budget: AnthropicStrictBudget): unknown | undefined {
|
|
4186
|
+
if (isRecord(schema)) {
|
|
4187
|
+
if (hasNullVariant(schema)) return schema;
|
|
4188
|
+
if (Array.isArray(schema.anyOf)) {
|
|
4189
|
+
return { ...schema, anyOf: [...schema.anyOf, { type: "null" }] };
|
|
4190
|
+
}
|
|
4191
|
+
if (Array.isArray(schema.type)) {
|
|
4192
|
+
return { ...schema, type: [...schema.type, "null"] };
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
if (budget.unionRemaining <= 0) return undefined;
|
|
4197
|
+
budget.unionRemaining--;
|
|
4198
|
+
budget.unionCount++;
|
|
4199
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
function normalizeAnthropicStrictSchemaNode(
|
|
4203
|
+
schema: unknown,
|
|
4204
|
+
budget: AnthropicStrictBudget,
|
|
4205
|
+
cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
|
|
4206
|
+
): unknown | undefined {
|
|
4207
|
+
if (Array.isArray(schema)) {
|
|
4208
|
+
const result: unknown[] = [];
|
|
4209
|
+
for (const entry of schema) {
|
|
4210
|
+
const normalized = normalizeAnthropicStrictSchemaNode(entry, budget, cache);
|
|
4211
|
+
if (normalized === undefined) return undefined;
|
|
4212
|
+
result.push(normalized);
|
|
4213
|
+
}
|
|
4214
|
+
return result;
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
if (!isRecord(schema)) return schema;
|
|
4218
|
+
|
|
4219
|
+
const cached = cache.get(schema);
|
|
4220
|
+
if (cached) return cached;
|
|
4221
|
+
|
|
4222
|
+
// Strict tool use only supports closed objects. Open maps stay available on
|
|
4223
|
+
// the non-strict schema plan instead of producing an Anthropic 400.
|
|
4224
|
+
if (isJsonSchemaObjectNode(schema) && schema.additionalProperties !== false) {
|
|
4225
|
+
return undefined;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
const result: Record<string, unknown> = { ...schema };
|
|
4229
|
+
cache.set(schema, result);
|
|
4230
|
+
|
|
4231
|
+
if (hasAnthropicUnionType(result)) {
|
|
4232
|
+
if (budget.unionRemaining <= 0) return undefined;
|
|
4233
|
+
budget.unionRemaining--;
|
|
4234
|
+
budget.unionCount++;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
if (isRecord(result.properties)) {
|
|
4238
|
+
const originalRequired = new Set(
|
|
4239
|
+
Array.isArray(result.required)
|
|
4240
|
+
? result.required.filter((entry): entry is string => typeof entry === "string")
|
|
4241
|
+
: [],
|
|
4242
|
+
);
|
|
4243
|
+
const properties: Record<string, unknown> = {};
|
|
4244
|
+
const required: string[] = [];
|
|
4245
|
+
|
|
4246
|
+
for (const [propertyName, propertySchema] of Object.entries(result.properties)) {
|
|
4247
|
+
const normalizedProperty = normalizeAnthropicStrictSchemaNode(propertySchema, budget, cache);
|
|
4248
|
+
if (normalizedProperty === undefined) return undefined;
|
|
4249
|
+
|
|
4250
|
+
if (originalRequired.has(propertyName)) {
|
|
4251
|
+
properties[propertyName] = normalizedProperty;
|
|
4252
|
+
required.push(propertyName);
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
if (budget.optionalRemaining > 0) {
|
|
4257
|
+
budget.optionalRemaining--;
|
|
4258
|
+
budget.optionalCount++;
|
|
4259
|
+
properties[propertyName] = normalizedProperty;
|
|
4260
|
+
continue;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
const nullableProperty = makeAnthropicNullableSchema(normalizedProperty, budget);
|
|
4264
|
+
if (nullableProperty === undefined) return undefined;
|
|
4265
|
+
properties[propertyName] = nullableProperty;
|
|
4266
|
+
required.push(propertyName);
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
result.properties = properties;
|
|
4270
|
+
result.required = required;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
if (Array.isArray(result.items)) {
|
|
4274
|
+
const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
|
|
4275
|
+
if (items === undefined) return undefined;
|
|
4276
|
+
result.items = items;
|
|
4277
|
+
} else if (isRecord(result.items)) {
|
|
4278
|
+
const items = normalizeAnthropicStrictSchemaNode(result.items, budget, cache);
|
|
4279
|
+
if (items === undefined) return undefined;
|
|
4280
|
+
result.items = items;
|
|
4281
|
+
}
|
|
4282
|
+
if (Array.isArray(result.prefixItems)) {
|
|
4283
|
+
const prefixItems = normalizeAnthropicStrictSchemaNode(result.prefixItems, budget, cache);
|
|
4284
|
+
if (prefixItems === undefined) return undefined;
|
|
4285
|
+
result.prefixItems = prefixItems;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
for (const key of COMBINATOR_KEYS) {
|
|
4289
|
+
const variants = result[key];
|
|
4290
|
+
if (!Array.isArray(variants)) continue;
|
|
4291
|
+
const normalizedVariants = normalizeAnthropicStrictSchemaNode(variants, budget, cache);
|
|
4292
|
+
if (normalizedVariants === undefined) return undefined;
|
|
4293
|
+
result[key] = normalizedVariants;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
for (const defsKey of ["$defs", "definitions"] as const) {
|
|
4297
|
+
const definitions = result[defsKey];
|
|
4298
|
+
if (!isRecord(definitions)) continue;
|
|
4299
|
+
const normalizedDefinitions: Record<string, unknown> = {};
|
|
4300
|
+
for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
|
|
4301
|
+
const normalizedDefinition = normalizeAnthropicStrictSchemaNode(definitionSchema, budget, cache);
|
|
4302
|
+
if (normalizedDefinition === undefined) return undefined;
|
|
4303
|
+
normalizedDefinitions[definitionName] = normalizedDefinition;
|
|
4304
|
+
}
|
|
4305
|
+
result[defsKey] = normalizedDefinitions;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
return result;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function normalizeAnthropicStrictSchema(
|
|
4312
|
+
schema: Record<string, unknown>,
|
|
4313
|
+
optionalRemaining: number,
|
|
4314
|
+
unionRemaining: number,
|
|
4315
|
+
): { schema: Record<string, unknown>; optionalCount: number; unionCount: number } | undefined {
|
|
4316
|
+
const budget: AnthropicStrictBudget = {
|
|
4317
|
+
optionalRemaining,
|
|
4318
|
+
unionRemaining,
|
|
4319
|
+
optionalCount: 0,
|
|
4320
|
+
unionCount: 0,
|
|
4321
|
+
};
|
|
4322
|
+
const normalized = normalizeAnthropicStrictSchemaNode(schema, budget, new WeakMap());
|
|
4323
|
+
if (!isRecord(normalized)) return undefined;
|
|
4324
|
+
return { schema: normalized, optionalCount: budget.optionalCount, unionCount: budget.unionCount };
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
function buildAnthropicBaseToolInputSchema(tool: Tool): Record<string, unknown> {
|
|
4328
|
+
const jsonSchema = toolWireSchema(tool);
|
|
4329
|
+
return flattenToolRootCombinators(
|
|
4330
|
+
normalizeAnthropicToolSchema({
|
|
4331
|
+
...jsonSchema,
|
|
4332
|
+
type: "object",
|
|
4333
|
+
properties: isRecord(jsonSchema.properties) ? jsonSchema.properties : {},
|
|
4334
|
+
required: Array.isArray(jsonSchema.required)
|
|
4335
|
+
? jsonSchema.required.filter((entry): entry is string => typeof entry === "string")
|
|
4336
|
+
: [],
|
|
4337
|
+
}) as Record<string, unknown>,
|
|
4338
|
+
);
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
function buildAnthropicToolSchemaPlans(tools: Tool[], disableStrictTools = false): AnthropicToolSchemaPlan[] {
|
|
4342
|
+
const plans = tools.map(
|
|
4343
|
+
(tool): AnthropicToolSchemaPlan => ({
|
|
4344
|
+
inputSchema: buildAnthropicBaseToolInputSchema(tool) as AnthropicToolInputSchema,
|
|
4345
|
+
strict: false,
|
|
4346
|
+
}),
|
|
4347
|
+
);
|
|
4348
|
+
if (NO_STRICT || disableStrictTools) return plans;
|
|
4349
|
+
|
|
4350
|
+
const candidateIndexes = tools.flatMap((tool, index) => {
|
|
4351
|
+
if (!ANTHROPIC_STRICT_TOOL_ALLOWLIST.has(tool.name)) return [];
|
|
4352
|
+
return tool.strict === false ? [] : [index];
|
|
4353
|
+
});
|
|
4354
|
+
|
|
4355
|
+
let strictToolCount = 0;
|
|
4356
|
+
let strictOptionalParameterCount = 0;
|
|
4357
|
+
let strictUnionParameterCount = 0;
|
|
4358
|
+
for (const index of candidateIndexes) {
|
|
4359
|
+
if (strictToolCount >= MAX_ANTHROPIC_STRICT_TOOLS) break;
|
|
4360
|
+
|
|
4361
|
+
const strictResult = normalizeAnthropicStrictSchema(
|
|
4362
|
+
plans[index].inputSchema as Record<string, unknown>,
|
|
4363
|
+
MAX_ANTHROPIC_STRICT_OPTIONAL_PARAMETERS - strictOptionalParameterCount,
|
|
4364
|
+
MAX_ANTHROPIC_STRICT_UNION_PARAMETERS - strictUnionParameterCount,
|
|
4365
|
+
);
|
|
4366
|
+
if (!strictResult) continue;
|
|
4367
|
+
|
|
4368
|
+
plans[index] = {
|
|
4369
|
+
inputSchema: strictResult.schema as AnthropicToolInputSchema,
|
|
4370
|
+
strict: true,
|
|
4371
|
+
};
|
|
4372
|
+
strictToolCount++;
|
|
4373
|
+
strictOptionalParameterCount += strictResult.optionalCount;
|
|
4374
|
+
strictUnionParameterCount += strictResult.unionCount;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
return plans;
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
function convertTools(
|
|
4381
|
+
tools: Tool[],
|
|
4382
|
+
isOAuthToken: boolean,
|
|
4383
|
+
disableStrictTools = false,
|
|
4384
|
+
supportsEagerToolInputStreaming = true,
|
|
4385
|
+
): Anthropic.Messages.Tool[] {
|
|
4386
|
+
if (!tools) return [];
|
|
4387
|
+
const schemaPlans = buildAnthropicToolSchemaPlans(tools, disableStrictTools);
|
|
4388
|
+
|
|
4389
|
+
return tools.map((tool, index) => {
|
|
4390
|
+
const plan = schemaPlans[index];
|
|
4391
|
+
return {
|
|
4392
|
+
name: isOAuthToken ? applyClaudeToolPrefix(tool.name) : tool.name,
|
|
4393
|
+
description: tool.description || "",
|
|
4394
|
+
input_schema: plan.inputSchema,
|
|
4395
|
+
...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
|
|
4396
|
+
...(plan.strict ? { strict: true } : {}),
|
|
4397
|
+
};
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
function mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason {
|
|
4402
|
+
switch (reason) {
|
|
4403
|
+
case "end_turn":
|
|
4404
|
+
return "stop";
|
|
4405
|
+
case "max_tokens":
|
|
4406
|
+
return "length";
|
|
4407
|
+
case "tool_use":
|
|
4408
|
+
return "toolUse";
|
|
4409
|
+
case "refusal":
|
|
4410
|
+
return "error";
|
|
4411
|
+
case "pause_turn": // Stop is good enough -> resubmit
|
|
4412
|
+
return "stop";
|
|
4413
|
+
case "stop_sequence":
|
|
4414
|
+
return "stop"; // We don't supply stop sequences, so this should never happen
|
|
4415
|
+
case "sensitive": // Content flagged by safety filters (not yet in SDK types)
|
|
4416
|
+
return "error";
|
|
4417
|
+
default:
|
|
4418
|
+
// Handle unknown stop reasons gracefully (API may add new values)
|
|
4419
|
+
throw new Error(`Unhandled stop reason: ${reason}`);
|
|
4420
|
+
}
|
|
4421
|
+
}
|