@aryee337/aery-ai 0.2.27 → 0.2.29
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 +2914 -0
- package/README.md +614 -813
- package/package.json +140 -105
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +818 -0
- package/src/auth-gateway/types.ts +143 -0
- package/src/auth-storage.ts +4422 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +469 -0
- package/src/model-thinking.ts +782 -0
- package/src/models.json +83530 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -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 +355 -0
- package/src/provider-models/google.ts +88 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2817 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/aery-native-client.ts +228 -0
- package/src/providers/aery-native-server.ts +212 -0
- package/src/providers/amazon-bedrock.ts +873 -0
- package/src/providers/anthropic-client.ts +318 -0
- package/src/providers/anthropic-messages-server-schema.ts +243 -0
- package/src/providers/anthropic-messages-server.ts +683 -0
- package/src/providers/anthropic-wire.ts +268 -0
- package/src/providers/anthropic.ts +3094 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +361 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2621 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +809 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +917 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +91 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +496 -0
- package/src/providers/ollama.ts +644 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +252 -0
- package/src/providers/openai-chat-server.ts +647 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +3018 -0
- package/src/providers/openai-completions-compat.ts +300 -0
- package/src/providers/openai-completions.ts +1979 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +873 -0
- package/src/providers/openai-responses.ts +679 -0
- package/src/providers/register-builtins.ts +436 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +382 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/providers/xai-responses.ts +82 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +1065 -0
- package/src/types.ts +944 -0
- package/src/usage/claude.ts +482 -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/kimi.ts +271 -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 +185 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/abortable-iterator.ts +69 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +267 -0
- package/src/utils/json-parse.ts +182 -0
- package/src/utils/oauth/__tests__/xai-oauth.test.ts +107 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +273 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -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/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -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 +484 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +23 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +203 -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/openrouter.ts +20 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -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/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.ts +99 -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 +40 -0
- package/src/utils/oauth/wafer.ts +50 -0
- package/src/utils/oauth/xai-oauth.ts +342 -0
- package/src/utils/oauth/xiaomi.ts +139 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/oauth/zhipu.ts +60 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/request-debug.ts +336 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +191 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +10 -0
- package/src/utils/schema/wire.ts +293 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sdk-stream-timeout.ts +43 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/stream-markup-healing.ts +612 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1024 -0
- package/src/utils.ts +166 -0
- package/dist/api-registry.d.ts +0 -20
- package/dist/api-registry.d.ts.map +0 -1
- package/dist/api-registry.js +0 -44
- package/dist/api-registry.js.map +0 -1
- package/dist/bedrock-provider.d.ts +0 -5
- package/dist/bedrock-provider.d.ts.map +0 -1
- package/dist/bedrock-provider.js +0 -6
- package/dist/bedrock-provider.js.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -130
- package/dist/cli.js.map +0 -1
- package/dist/env-api-keys.d.ts +0 -18
- package/dist/env-api-keys.d.ts.map +0 -1
- package/dist/env-api-keys.js +0 -178
- package/dist/env-api-keys.js.map +0 -1
- package/dist/image-models.d.ts +0 -10
- package/dist/image-models.d.ts.map +0 -1
- package/dist/image-models.generated.d.ts +0 -440
- package/dist/image-models.generated.d.ts.map +0 -1
- package/dist/image-models.generated.js +0 -442
- package/dist/image-models.generated.js.map +0 -1
- package/dist/image-models.js +0 -23
- package/dist/image-models.js.map +0 -1
- package/dist/images-api-registry.d.ts +0 -14
- package/dist/images-api-registry.d.ts.map +0 -1
- package/dist/images-api-registry.js +0 -22
- package/dist/images-api-registry.js.map +0 -1
- package/dist/images.d.ts +0 -4
- package/dist/images.d.ts.map +0 -1
- package/dist/images.js +0 -14
- package/dist/images.js.map +0 -1
- package/dist/index.d.ts +0 -32
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -20
- package/dist/index.js.map +0 -1
- package/dist/models.d.ts +0 -18
- package/dist/models.d.ts.map +0 -1
- package/dist/models.generated.d.ts +0 -17707
- package/dist/models.generated.d.ts.map +0 -1
- package/dist/models.generated.js +0 -16561
- package/dist/models.generated.js.map +0 -1
- package/dist/models.js +0 -71
- package/dist/models.js.map +0 -1
- package/dist/oauth.d.ts +0 -2
- package/dist/oauth.d.ts.map +0 -1
- package/dist/oauth.js +0 -2
- package/dist/oauth.js.map +0 -1
- package/dist/providers/aery-error-formatting.d.ts +0 -13
- package/dist/providers/aery-error-formatting.d.ts.map +0 -1
- package/dist/providers/aery-error-formatting.js +0 -112
- package/dist/providers/aery-error-formatting.js.map +0 -1
- package/dist/providers/amazon-bedrock.d.ts +0 -38
- package/dist/providers/amazon-bedrock.d.ts.map +0 -1
- package/dist/providers/amazon-bedrock.js +0 -763
- package/dist/providers/amazon-bedrock.js.map +0 -1
- package/dist/providers/anthropic.d.ts +0 -71
- package/dist/providers/anthropic.d.ts.map +0 -1
- package/dist/providers/anthropic.js +0 -949
- package/dist/providers/anthropic.js.map +0 -1
- package/dist/providers/azure-openai-responses.d.ts +0 -15
- package/dist/providers/azure-openai-responses.d.ts.map +0 -1
- package/dist/providers/azure-openai-responses.js +0 -225
- package/dist/providers/azure-openai-responses.js.map +0 -1
- package/dist/providers/cloudflare.d.ts +0 -13
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js +0 -26
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/faux.d.ts +0 -56
- package/dist/providers/faux.d.ts.map +0 -1
- package/dist/providers/faux.js +0 -368
- package/dist/providers/faux.js.map +0 -1
- package/dist/providers/github-copilot-headers.d.ts +0 -8
- package/dist/providers/github-copilot-headers.d.ts.map +0 -1
- package/dist/providers/github-copilot-headers.js +0 -29
- package/dist/providers/github-copilot-headers.js.map +0 -1
- package/dist/providers/google-gemini-cli.d.ts +0 -74
- package/dist/providers/google-gemini-cli.d.ts.map +0 -1
- package/dist/providers/google-gemini-cli.js +0 -779
- package/dist/providers/google-gemini-cli.js.map +0 -1
- package/dist/providers/google-shared.d.ts +0 -70
- package/dist/providers/google-shared.d.ts.map +0 -1
- package/dist/providers/google-shared.js +0 -329
- package/dist/providers/google-shared.js.map +0 -1
- package/dist/providers/google-vertex.d.ts +0 -15
- package/dist/providers/google-vertex.d.ts.map +0 -1
- package/dist/providers/google-vertex.js +0 -442
- package/dist/providers/google-vertex.js.map +0 -1
- package/dist/providers/google.d.ts +0 -13
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers/google.js +0 -400
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/images/openrouter.d.ts +0 -3
- package/dist/providers/images/openrouter.d.ts.map +0 -1
- package/dist/providers/images/openrouter.js +0 -129
- package/dist/providers/images/openrouter.js.map +0 -1
- package/dist/providers/images/register-builtins.d.ts +0 -4
- package/dist/providers/images/register-builtins.d.ts.map +0 -1
- package/dist/providers/images/register-builtins.js +0 -34
- package/dist/providers/images/register-builtins.js.map +0 -1
- package/dist/providers/mistral.d.ts +0 -25
- package/dist/providers/mistral.d.ts.map +0 -1
- package/dist/providers/mistral.js +0 -535
- package/dist/providers/mistral.js.map +0 -1
- package/dist/providers/openai-codex-responses.d.ts +0 -30
- package/dist/providers/openai-codex-responses.d.ts.map +0 -1
- package/dist/providers/openai-codex-responses.js +0 -1090
- package/dist/providers/openai-codex-responses.js.map +0 -1
- package/dist/providers/openai-completions.d.ts +0 -19
- package/dist/providers/openai-completions.d.ts.map +0 -1
- package/dist/providers/openai-completions.js +0 -950
- package/dist/providers/openai-completions.js.map +0 -1
- package/dist/providers/openai-prompt-cache.d.ts +0 -3
- package/dist/providers/openai-prompt-cache.d.ts.map +0 -1
- package/dist/providers/openai-prompt-cache.js +0 -10
- package/dist/providers/openai-prompt-cache.js.map +0 -1
- package/dist/providers/openai-responses-shared.d.ts +0 -18
- package/dist/providers/openai-responses-shared.d.ts.map +0 -1
- package/dist/providers/openai-responses-shared.js +0 -492
- package/dist/providers/openai-responses-shared.js.map +0 -1
- package/dist/providers/openai-responses.d.ts +0 -13
- package/dist/providers/openai-responses.d.ts.map +0 -1
- package/dist/providers/openai-responses.js +0 -237
- package/dist/providers/openai-responses.js.map +0 -1
- package/dist/providers/register-builtins.d.ts +0 -38
- package/dist/providers/register-builtins.d.ts.map +0 -1
- package/dist/providers/register-builtins.js +0 -278
- package/dist/providers/register-builtins.js.map +0 -1
- package/dist/providers/simple-options.d.ts +0 -8
- package/dist/providers/simple-options.d.ts.map +0 -1
- package/dist/providers/simple-options.js +0 -41
- package/dist/providers/simple-options.js.map +0 -1
- package/dist/providers/transform-messages.d.ts +0 -8
- package/dist/providers/transform-messages.d.ts.map +0 -1
- package/dist/providers/transform-messages.js +0 -184
- package/dist/providers/transform-messages.js.map +0 -1
- package/dist/session-resources.d.ts +0 -4
- package/dist/session-resources.d.ts.map +0 -1
- package/dist/session-resources.js +0 -22
- package/dist/session-resources.js.map +0 -1
- package/dist/stream.d.ts +0 -8
- package/dist/stream.d.ts.map +0 -1
- package/dist/stream.js +0 -27
- package/dist/stream.js.map +0 -1
- package/dist/types.d.ts +0 -498
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils/diagnostics.d.ts +0 -19
- package/dist/utils/diagnostics.d.ts.map +0 -1
- package/dist/utils/diagnostics.js +0 -25
- package/dist/utils/diagnostics.js.map +0 -1
- package/dist/utils/event-stream.d.ts +0 -21
- package/dist/utils/event-stream.d.ts.map +0 -1
- package/dist/utils/event-stream.js +0 -81
- package/dist/utils/event-stream.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -3
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -14
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/headers.d.ts +0 -2
- package/dist/utils/headers.d.ts.map +0 -1
- package/dist/utils/headers.js +0 -8
- package/dist/utils/headers.js.map +0 -1
- package/dist/utils/json-parse.d.ts +0 -16
- package/dist/utils/json-parse.d.ts.map +0 -1
- package/dist/utils/json-parse.js +0 -113
- package/dist/utils/json-parse.js.map +0 -1
- package/dist/utils/node-http-proxy.d.ts +0 -10
- package/dist/utils/node-http-proxy.d.ts.map +0 -1
- package/dist/utils/node-http-proxy.js +0 -97
- package/dist/utils/node-http-proxy.js.map +0 -1
- package/dist/utils/oauth/anthropic.d.ts +0 -25
- package/dist/utils/oauth/anthropic.d.ts.map +0 -1
- package/dist/utils/oauth/anthropic.js +0 -335
- package/dist/utils/oauth/anthropic.js.map +0 -1
- package/dist/utils/oauth/device-code.d.ts +0 -19
- package/dist/utils/oauth/device-code.d.ts.map +0 -1
- package/dist/utils/oauth/device-code.js +0 -55
- package/dist/utils/oauth/device-code.js.map +0 -1
- package/dist/utils/oauth/github-copilot.d.ts +0 -30
- package/dist/utils/oauth/github-copilot.d.ts.map +0 -1
- package/dist/utils/oauth/github-copilot.js +0 -268
- package/dist/utils/oauth/github-copilot.js.map +0 -1
- package/dist/utils/oauth/google-antigravity.d.ts +0 -26
- package/dist/utils/oauth/google-antigravity.d.ts.map +0 -1
- package/dist/utils/oauth/google-antigravity.js +0 -377
- package/dist/utils/oauth/google-antigravity.js.map +0 -1
- package/dist/utils/oauth/google-gemini-cli.d.ts +0 -26
- package/dist/utils/oauth/google-gemini-cli.d.ts.map +0 -1
- package/dist/utils/oauth/google-gemini-cli.js +0 -482
- package/dist/utils/oauth/google-gemini-cli.js.map +0 -1
- package/dist/utils/oauth/index.d.ts +0 -63
- package/dist/utils/oauth/index.d.ts.map +0 -1
- package/dist/utils/oauth/index.js +0 -131
- package/dist/utils/oauth/index.js.map +0 -1
- package/dist/utils/oauth/oauth-page.d.ts +0 -3
- package/dist/utils/oauth/oauth-page.d.ts.map +0 -1
- package/dist/utils/oauth/oauth-page.js +0 -105
- package/dist/utils/oauth/oauth-page.js.map +0 -1
- package/dist/utils/oauth/openai-codex.d.ts +0 -34
- package/dist/utils/oauth/openai-codex.d.ts.map +0 -1
- package/dist/utils/oauth/openai-codex.js +0 -385
- package/dist/utils/oauth/openai-codex.js.map +0 -1
- package/dist/utils/oauth/pkce.d.ts +0 -13
- package/dist/utils/oauth/pkce.d.ts.map +0 -1
- package/dist/utils/oauth/pkce.js +0 -31
- package/dist/utils/oauth/pkce.js.map +0 -1
- package/dist/utils/oauth/types.d.ts +0 -64
- package/dist/utils/oauth/types.d.ts.map +0 -1
- package/dist/utils/oauth/types.js +0 -2
- package/dist/utils/oauth/types.js.map +0 -1
- package/dist/utils/overflow.d.ts +0 -56
- package/dist/utils/overflow.d.ts.map +0 -1
- package/dist/utils/overflow.js +0 -151
- package/dist/utils/overflow.js.map +0 -1
- package/dist/utils/sanitize-unicode.d.ts +0 -22
- package/dist/utils/sanitize-unicode.d.ts.map +0 -1
- package/dist/utils/sanitize-unicode.js +0 -26
- package/dist/utils/sanitize-unicode.js.map +0 -1
- package/dist/utils/typebox-helpers.d.ts +0 -17
- package/dist/utils/typebox-helpers.d.ts.map +0 -1
- package/dist/utils/typebox-helpers.js +0 -21
- package/dist/utils/typebox-helpers.js.map +0 -1
- package/dist/utils/validation.d.ts +0 -18
- package/dist/utils/validation.d.ts.map +0 -1
- package/dist/utils/validation.js +0 -281
- package/dist/utils/validation.js.map +0 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import turnAbortedGuidance from "../prompts/turn-aborted-guidance.md" with { type: "text" };
|
|
2
|
+
import type {
|
|
3
|
+
Api,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
DeveloperMessage,
|
|
6
|
+
Message,
|
|
7
|
+
Model,
|
|
8
|
+
ToolCall,
|
|
9
|
+
ToolResultMessage,
|
|
10
|
+
UserMessage,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
const enum ToolCallStatus {
|
|
14
|
+
/** A tool result has already been emitted for this tool call; later duplicates must be skipped. */
|
|
15
|
+
Resolved = 1,
|
|
16
|
+
/** A synthetic aborted result was emitted; later real results must be skipped. */
|
|
17
|
+
Aborted = 2,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldDropTruncatedThinkingOnlyAssistant(msg: AssistantMessage): boolean {
|
|
21
|
+
const isTruncatedStop = msg.stopReason === "length" || msg.stopReason === "error" || msg.stopReason === "aborted";
|
|
22
|
+
return isTruncatedStop && !msg.content.some(block => block.type === "toolCall" || block.type === "text");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getLatestSurvivingAssistantIndex(messages: readonly Message[]): number {
|
|
26
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const msg = messages[index]!;
|
|
28
|
+
if (msg.role === "assistant" && !shouldDropTruncatedThinkingOnlyAssistant(msg)) {
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize tool call ID for cross-provider compatibility.
|
|
37
|
+
* OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
|
|
38
|
+
* Anthropic APIs require IDs matching ^[a-zA-Z0-9_-]+$ (max 64 chars).
|
|
39
|
+
*
|
|
40
|
+
* For aborted/errored turns, this function:
|
|
41
|
+
* - Preserves tool call structure (unlike converting to text summaries)
|
|
42
|
+
* - Injects synthetic "aborted" tool results
|
|
43
|
+
* - Adds a <turn-aborted> guidance marker for the model
|
|
44
|
+
*/
|
|
45
|
+
export function transformMessages<TApi extends Api>(
|
|
46
|
+
messages: Message[],
|
|
47
|
+
model: Model<TApi>,
|
|
48
|
+
normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string,
|
|
49
|
+
): Message[] {
|
|
50
|
+
// Build a map of original tool call IDs to normalized IDs
|
|
51
|
+
const toolCallIdMap = new Map<string, string>();
|
|
52
|
+
|
|
53
|
+
const latestSurvivingAssistantIndex = getLatestSurvivingAssistantIndex(messages);
|
|
54
|
+
// First pass: transform messages (thinking blocks, tool call ID normalization)
|
|
55
|
+
const transformed = messages.map((msg, index) => {
|
|
56
|
+
// User and developer messages pass through unchanged
|
|
57
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
58
|
+
return msg;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle toolResult messages - normalize toolCallId if we have a mapping
|
|
62
|
+
if (msg.role === "toolResult") {
|
|
63
|
+
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
64
|
+
if (normalizedId && normalizedId !== msg.toolCallId) {
|
|
65
|
+
return { ...msg, toolCallId: normalizedId };
|
|
66
|
+
}
|
|
67
|
+
return msg;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Assistant messages need transformation check
|
|
71
|
+
if (msg.role === "assistant") {
|
|
72
|
+
const assistantMsg = msg as AssistantMessage;
|
|
73
|
+
const isSameModel =
|
|
74
|
+
assistantMsg.provider === model.provider &&
|
|
75
|
+
assistantMsg.api === model.api &&
|
|
76
|
+
assistantMsg.model === model.id;
|
|
77
|
+
|
|
78
|
+
const mustPreserveLatestAnthropicThinking =
|
|
79
|
+
index === latestSurvivingAssistantIndex &&
|
|
80
|
+
model.api === "anthropic-messages" &&
|
|
81
|
+
assistantMsg.api === "anthropic-messages";
|
|
82
|
+
// Aborted/errored messages may have partially-streamed thinking signatures.
|
|
83
|
+
// A partial signature is invalid and will be rejected by the API, so we must
|
|
84
|
+
// strip signatures from thinking blocks in these messages.
|
|
85
|
+
//
|
|
86
|
+
// Abandoned tool-use turns get the same treatment once they are no longer
|
|
87
|
+
// the latest assistant message. When a turn carries toolCall blocks but did
|
|
88
|
+
// NOT request tool execution (stopReason !== "toolUse" — e.g.
|
|
89
|
+
// adaptive-thinking Opus emitting tool calls and then ending the turn on
|
|
90
|
+
// `end_turn`/`stop`), the agent loop pairs those calls with placeholder
|
|
91
|
+
// tool_results to keep the tool_use/tool_result contract valid. Historical
|
|
92
|
+
// abandoned turns cannot safely replay their end_turn-bound signatures in
|
|
93
|
+
// that continuation, so stripping downgrades them to plain text downstream.
|
|
94
|
+
// Latest abandoned turns are exempt because Anthropic requires thinking
|
|
95
|
+
// blocks from its most recent response to remain byte-for-byte unmodified.
|
|
96
|
+
const invalidStopReason = assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error";
|
|
97
|
+
const abandonedToolUse =
|
|
98
|
+
!invalidStopReason &&
|
|
99
|
+
assistantMsg.stopReason !== "toolUse" &&
|
|
100
|
+
assistantMsg.content.some(b => b.type === "toolCall");
|
|
101
|
+
const hasInvalidSignatures = invalidStopReason || abandonedToolUse;
|
|
102
|
+
|
|
103
|
+
const transformedContent = assistantMsg.content.flatMap(block => {
|
|
104
|
+
if (block.type === "thinking") {
|
|
105
|
+
// Strip untrustworthy signatures so the encoder can downgrade to text.
|
|
106
|
+
const sanitized =
|
|
107
|
+
hasInvalidSignatures && block.thinkingSignature ? { ...block, thinkingSignature: undefined } : block;
|
|
108
|
+
if (mustPreserveLatestAnthropicThinking) return abandonedToolUse ? block : sanitized;
|
|
109
|
+
// For same model: keep thinking blocks with signatures (needed for replay)
|
|
110
|
+
// even if the thinking text is empty (OpenAI encrypted reasoning)
|
|
111
|
+
if (isSameModel && sanitized.thinkingSignature) return sanitized;
|
|
112
|
+
// Skip empty thinking blocks, convert others to plain text
|
|
113
|
+
if (!sanitized.thinking || sanitized.thinking.trim() === "") return [];
|
|
114
|
+
if (isSameModel) return sanitized;
|
|
115
|
+
return {
|
|
116
|
+
type: "text" as const,
|
|
117
|
+
text: sanitized.thinking,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (block.type === "redactedThinking") {
|
|
122
|
+
if (mustPreserveLatestAnthropicThinking) return block;
|
|
123
|
+
if (isSameModel) return block;
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (block.type === "text") {
|
|
128
|
+
if (isSameModel) return block;
|
|
129
|
+
return {
|
|
130
|
+
type: "text" as const,
|
|
131
|
+
text: block.text,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (block.type === "toolCall") {
|
|
136
|
+
const toolCall = block as ToolCall;
|
|
137
|
+
let normalizedToolCall: ToolCall = toolCall;
|
|
138
|
+
|
|
139
|
+
if (!isSameModel && toolCall.thoughtSignature) {
|
|
140
|
+
normalizedToolCall = { ...toolCall };
|
|
141
|
+
delete (normalizedToolCall as { thoughtSignature?: string }).thoughtSignature;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
145
|
+
const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMsg);
|
|
146
|
+
if (normalizedId !== toolCall.id) {
|
|
147
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
148
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return normalizedToolCall;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return block;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...assistantMsg,
|
|
160
|
+
content: transformedContent,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return msg;
|
|
164
|
+
});
|
|
165
|
+
const realToolResultsById = new Map<string, ToolResultMessage>();
|
|
166
|
+
for (const msg of transformed) {
|
|
167
|
+
if (msg.role === "toolResult" && !realToolResultsById.has(msg.toolCallId)) {
|
|
168
|
+
realToolResultsById.set(msg.toolCallId, msg);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Anthropic rejects `tool_result` blocks whose `tool_use_id` does not appear in a prior
|
|
173
|
+
// `tool_use` block. After handoff/compaction folds an assistant turn into a summary
|
|
174
|
+
// string, the user-side `toolResult` for that turn can survive while the originating
|
|
175
|
+
// `tool_use` disappears — leaving an orphan that triggers HTTP 400. Track the set of
|
|
176
|
+
// `tool_use` ids that survive transformation so the second pass can drop orphans cleanly.
|
|
177
|
+
const validToolUseIds = new Set<string>();
|
|
178
|
+
for (const msg of transformed) {
|
|
179
|
+
if (msg.role !== "assistant") continue;
|
|
180
|
+
for (const block of msg.content) {
|
|
181
|
+
if (block.type === "toolCall") validToolUseIds.add(block.id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Second pass: ensure each surviving assistant tool call is immediately
|
|
186
|
+
// followed by exactly one corresponding tool result.
|
|
187
|
+
const result: Message[] = [];
|
|
188
|
+
let pendingToolCalls: ToolCall[] = [];
|
|
189
|
+
let pendingAbortedToolCalls = new Map<string, ToolCall>();
|
|
190
|
+
let pendingAbortedTimestamp: number | undefined;
|
|
191
|
+
// Track which tool calls already have an emitted result so delayed/duplicate
|
|
192
|
+
// toolResult messages cannot create a second provider-visible result.
|
|
193
|
+
const toolCallStatus = new Map<string, ToolCallStatus>();
|
|
194
|
+
|
|
195
|
+
const flushPendingToolCalls = (timestamp: number): void => {
|
|
196
|
+
if (pendingToolCalls.length === 0) return;
|
|
197
|
+
for (const tc of pendingToolCalls) {
|
|
198
|
+
if (toolCallStatus.has(tc.id)) continue;
|
|
199
|
+
const realToolResult = realToolResultsById.get(tc.id);
|
|
200
|
+
if (realToolResult) {
|
|
201
|
+
result.push(realToolResult);
|
|
202
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
result.push({
|
|
206
|
+
role: "toolResult",
|
|
207
|
+
toolCallId: tc.id,
|
|
208
|
+
toolName: tc.name,
|
|
209
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
210
|
+
isError: true,
|
|
211
|
+
timestamp,
|
|
212
|
+
} as ToolResultMessage);
|
|
213
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
214
|
+
}
|
|
215
|
+
pendingToolCalls = [];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const flushPendingAbortedToolCalls = (): void => {
|
|
219
|
+
if (pendingAbortedTimestamp === undefined) return;
|
|
220
|
+
for (const tc of pendingAbortedToolCalls.values()) {
|
|
221
|
+
if (toolCallStatus.has(tc.id)) continue;
|
|
222
|
+
const realToolResult = realToolResultsById.get(tc.id);
|
|
223
|
+
if (realToolResult) {
|
|
224
|
+
result.push(realToolResult);
|
|
225
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
result.push({
|
|
229
|
+
role: "toolResult",
|
|
230
|
+
toolCallId: tc.id,
|
|
231
|
+
toolName: tc.name,
|
|
232
|
+
content: [{ type: "text", text: "aborted" }],
|
|
233
|
+
isError: true,
|
|
234
|
+
timestamp: pendingAbortedTimestamp,
|
|
235
|
+
} as ToolResultMessage);
|
|
236
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
|
|
237
|
+
}
|
|
238
|
+
result.push({
|
|
239
|
+
role: "developer",
|
|
240
|
+
content: turnAbortedGuidance,
|
|
241
|
+
timestamp: pendingAbortedTimestamp + 1,
|
|
242
|
+
} as DeveloperMessage);
|
|
243
|
+
pendingAbortedToolCalls = new Map();
|
|
244
|
+
pendingAbortedTimestamp = undefined;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < transformed.length; i++) {
|
|
248
|
+
const msg = transformed[i];
|
|
249
|
+
const messageTimestamp = "timestamp" in msg && typeof msg.timestamp === "number" ? msg.timestamp : Date.now();
|
|
250
|
+
|
|
251
|
+
if (msg.role === "assistant") {
|
|
252
|
+
flushPendingToolCalls(messageTimestamp);
|
|
253
|
+
flushPendingAbortedToolCalls();
|
|
254
|
+
|
|
255
|
+
const assistantMsg = msg as AssistantMessage;
|
|
256
|
+
|
|
257
|
+
// Drop assistant turns that carry no actionable content (no `text`, no `toolCall`)
|
|
258
|
+
// AND were terminated by a truncating stop reason (`length` / `error` / `aborted`).
|
|
259
|
+
// These are produced when the provider returns `stop_reason: "max_tokens"` (or a
|
|
260
|
+
// stream error) mid-thinking, leaving a `[thinking]`-only message with a valid
|
|
261
|
+
// signature but nothing for the next turn to anchor on. Keeping it creates
|
|
262
|
+
// back-to-back assistant turns once the next response lands, which Anthropic
|
|
263
|
+
// rejects with "messages.X.content.Y: `thinking` blocks in the latest assistant
|
|
264
|
+
// message cannot be modified".
|
|
265
|
+
//
|
|
266
|
+
// `stopReason: "stop"` thinking-only messages are intentionally preserved: they
|
|
267
|
+
// represent reasoning-only assistant turns used for replay round-trips
|
|
268
|
+
// (OpenAI completions `reasoning_text`, Google signed thought parts).
|
|
269
|
+
const originalMsg = messages[i]!;
|
|
270
|
+
if (originalMsg.role === "assistant" && shouldDropTruncatedThinkingOnlyAssistant(originalMsg)) {
|
|
271
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
272
|
+
// Still arm the aborted-turn note so downstream guidance fires.
|
|
273
|
+
pendingAbortedToolCalls = new Map();
|
|
274
|
+
pendingAbortedTimestamp = assistantMsg.timestamp;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
|
|
280
|
+
|
|
281
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
282
|
+
// Keep the assistant message with tool calls intact. Real tool results are
|
|
283
|
+
// emitted immediately if available; otherwise synthesize aborted results
|
|
284
|
+
// before the next turn boundary.
|
|
285
|
+
result.push(msg);
|
|
286
|
+
pendingAbortedToolCalls = new Map(toolCalls.map(toolCall => [toolCall.id, toolCall] as const));
|
|
287
|
+
pendingAbortedTimestamp = assistantMsg.timestamp;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (toolCalls.length > 0) {
|
|
292
|
+
pendingToolCalls = toolCalls;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
result.push(msg);
|
|
296
|
+
} else if (msg.role === "toolResult") {
|
|
297
|
+
if (toolCallStatus.has(msg.toolCallId)) continue;
|
|
298
|
+
|
|
299
|
+
if (pendingAbortedToolCalls.has(msg.toolCallId)) {
|
|
300
|
+
pendingAbortedToolCalls.delete(msg.toolCallId);
|
|
301
|
+
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
302
|
+
result.push(msg);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (pendingToolCalls.some(tc => tc.id === msg.toolCallId)) {
|
|
307
|
+
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
308
|
+
result.push(msg);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!validToolUseIds.has(msg.toolCallId)) {
|
|
313
|
+
// Orphan `tool_result`: the originating `tool_use` is not present in the
|
|
314
|
+
// transformed history (typically because handoff/compaction folded the
|
|
315
|
+
// assistant message into a summary string while the user-side result
|
|
316
|
+
// survived). Sending the block as-is would 400 the request, so it must
|
|
317
|
+
// be dropped.
|
|
318
|
+
//
|
|
319
|
+
// If a pending tool-call window is still open (either normal or
|
|
320
|
+
// aborted), the orphan cannot be replaced with a developer note here:
|
|
321
|
+
//
|
|
322
|
+
// * Anthropic requires the next message after an assistant `tool_use`
|
|
323
|
+
// to be the matching `tool_result`. Inserting a developer message
|
|
324
|
+
// would break that contiguity.
|
|
325
|
+
// * Flushing pending aborted calls here would wedge synthetic results
|
|
326
|
+
// between the assistant turn and a real result that may still arrive
|
|
327
|
+
// inside the current contiguous result window.
|
|
328
|
+
//
|
|
329
|
+
// Drop the orphan silently in that case; the pending calls will be
|
|
330
|
+
// resolved in their own contiguous result window or at the next boundary.
|
|
331
|
+
if (pendingToolCalls.some(tc => !toolCallStatus.has(tc.id)) || pendingAbortedToolCalls.size > 0) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
// No pending tool-call window: safe to preserve the text payload so the
|
|
335
|
+
// model still sees what the tool returned.
|
|
336
|
+
//
|
|
337
|
+
// The note is emitted with `role: "user"` rather than `role: "developer"`
|
|
338
|
+
// because the developer role is elevated by some providers:
|
|
339
|
+
//
|
|
340
|
+
// * Ollama maps `developer` -> `system` (highest instruction priority).
|
|
341
|
+
// * OpenAI chat-completions reasoning models forward `developer` as
|
|
342
|
+
// `developer` (above-user instruction priority).
|
|
343
|
+
//
|
|
344
|
+
// Stale, model-untrusted tool output must not gain instruction priority
|
|
345
|
+
// above user/developer messages it lived alongside before compaction.
|
|
346
|
+
// `user` role is mapped to plain user content by every provider, so the
|
|
347
|
+
// content survives without ever being treated as an instruction the
|
|
348
|
+
// model should obey.
|
|
349
|
+
const textParts: string[] = [];
|
|
350
|
+
for (const part of msg.content) {
|
|
351
|
+
if (part.type === "text" && part.text.trim() !== "") textParts.push(part.text);
|
|
352
|
+
}
|
|
353
|
+
if (textParts.length > 0) {
|
|
354
|
+
const errorAttr = msg.isError ? ' is-error="true"' : "";
|
|
355
|
+
result.push({
|
|
356
|
+
role: "user",
|
|
357
|
+
content: `<stale-tool-result tool="${msg.toolName}" id="${msg.toolCallId}"${errorAttr}>\n${textParts.join("\n")}\n</stale-tool-result>`,
|
|
358
|
+
timestamp: messageTimestamp,
|
|
359
|
+
} as UserMessage);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The matching tool_use exists elsewhere, but this result is not in
|
|
364
|
+
// the currently open result window. Emitting it here would break the
|
|
365
|
+
// provider invariant; the first real result is pulled into the correct
|
|
366
|
+
// slot by the pending-call flush instead.
|
|
367
|
+
} else if (msg.role === "user" || msg.role === "developer") {
|
|
368
|
+
flushPendingToolCalls(messageTimestamp);
|
|
369
|
+
flushPendingAbortedToolCalls();
|
|
370
|
+
result.push(msg);
|
|
371
|
+
} else {
|
|
372
|
+
flushPendingToolCalls(messageTimestamp);
|
|
373
|
+
flushPendingAbortedToolCalls();
|
|
374
|
+
result.push(msg);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
flushPendingToolCalls(Date.now());
|
|
379
|
+
flushPendingAbortedToolCalls();
|
|
380
|
+
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "../types";
|
|
2
|
+
|
|
3
|
+
export const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]";
|
|
4
|
+
|
|
5
|
+
export function partitionVisionContent(
|
|
6
|
+
content: ReadonlyArray<TextContent | ImageContent>,
|
|
7
|
+
supportsImages: boolean,
|
|
8
|
+
): {
|
|
9
|
+
textBlocks: TextContent[];
|
|
10
|
+
imageBlocks: ImageContent[];
|
|
11
|
+
omittedImages: boolean;
|
|
12
|
+
} {
|
|
13
|
+
const textBlocks = content.filter((block): block is TextContent => block.type === "text");
|
|
14
|
+
const imageBlocks = content.filter((block): block is ImageContent => block.type === "image");
|
|
15
|
+
return {
|
|
16
|
+
textBlocks,
|
|
17
|
+
imageBlocks: supportsImages ? imageBlocks : [],
|
|
18
|
+
omittedImages: !supportsImages && imageBlocks.length > 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function joinTextWithImagePlaceholder(text: string, omittedImages: boolean): string {
|
|
23
|
+
const parts: string[] = [];
|
|
24
|
+
if (text.length > 0) {
|
|
25
|
+
parts.push(text);
|
|
26
|
+
}
|
|
27
|
+
if (omittedImages) {
|
|
28
|
+
parts.push(NON_VISION_IMAGE_PLACEHOLDER);
|
|
29
|
+
}
|
|
30
|
+
return parts.join("\n");
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Ported from NousResearch/hermes-agent (MIT) — agent/transports/codex.py:182-193,
|
|
2
|
+
// agent/codex_responses_adapter.py:247-311, agent/model_metadata.py:263-285.
|
|
3
|
+
// Logic EXTRACTED into a dedicated xAI adapter so the generic OpenAI Responses
|
|
4
|
+
// path stays provider-agnostic and the OpenAI Codex Responses path is unaffected.
|
|
5
|
+
|
|
6
|
+
import type { Context, Model, StreamFunction } from "../types";
|
|
7
|
+
import {
|
|
8
|
+
getOpenAIResponsesCacheSessionId,
|
|
9
|
+
type OpenAIResponsesOptions,
|
|
10
|
+
streamOpenAIResponses,
|
|
11
|
+
} from "./openai-responses";
|
|
12
|
+
|
|
13
|
+
// xAI rejects `reasoning.effort` on grok-4 / grok-4-fast / grok-3 /
|
|
14
|
+
// grok-code-fast / grok-4.20-0309-* / grok-build with HTTP 400 ("Model X does
|
|
15
|
+
// not support parameter reasoningEffort") even though those models reason
|
|
16
|
+
// natively (hermes-agent/agent/transports/codex.py:127-133). Only send the
|
|
17
|
+
// effort dial when the target model is on this allowlist; otherwise suppress
|
|
18
|
+
// it via OpenAIResponsesOptions.omitReasoningEffort and let the model reason
|
|
19
|
+
// on its own. grok-build was previously on this list per user spec; the live
|
|
20
|
+
// xAI server contradicts that assumption (HTTP 400 confirmed against
|
|
21
|
+
// api.x.ai/v1/responses on 2026-05-17).
|
|
22
|
+
const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3"] as const;
|
|
23
|
+
|
|
24
|
+
function grokSupportsReasoningEffort(modelId: string): boolean {
|
|
25
|
+
const name = (modelId || "").trim().toLowerCase();
|
|
26
|
+
if (!name) return false;
|
|
27
|
+
// Strip common aggregator prefixes (x-ai/, openrouter/x-ai/, xai/, ...) before matching.
|
|
28
|
+
const bare = name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
|
|
29
|
+
return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* xAI Grok Responses adapter (SuperGrok OAuth path).
|
|
34
|
+
*
|
|
35
|
+
* Three xAI-specific behaviors vs the generic OpenAI Responses adapter:
|
|
36
|
+
*
|
|
37
|
+
* 1. `x-grok-conv-id` header + body `prompt_cache_key` route prompt-cache
|
|
38
|
+
* hits on xAI's edge. Hermes uses both (agent/transports/codex.py:182-193).
|
|
39
|
+
* The header is undocumented by xAI; `previous_response_id` is the
|
|
40
|
+
* documented alternative — switch if xAI deprecates the header.
|
|
41
|
+
* 2. includeEncryptedReasoning=false — xAI's /v1/responses rejects replayed
|
|
42
|
+
* `encrypted_content` blobs minted under SuperGrok OAuth.
|
|
43
|
+
* 3. filterReasoningHistory=true — strip `type: "reasoning"` items from
|
|
44
|
+
* replayed conversation history; the blob inside is non-replayable under
|
|
45
|
+
* OAuth and the wrapper item 404s without it (store=false; server cannot
|
|
46
|
+
* resolve by id).
|
|
47
|
+
*
|
|
48
|
+
* Everything else is the generic OpenAI Responses transport. The xAI bearer
|
|
49
|
+
* token arrives in `options.apiKey` via AuthStorage.getApiKey() upstream, and
|
|
50
|
+
* the xAI base URL (`https://api.x.ai/v1`) arrives via `model.baseUrl` from
|
|
51
|
+
* the provider registry — not routed through this wrapper.
|
|
52
|
+
*/
|
|
53
|
+
export const streamXAIResponses: StreamFunction<"openai-responses"> = (
|
|
54
|
+
model: Model<"openai-responses">,
|
|
55
|
+
context: Context,
|
|
56
|
+
options: OpenAIResponsesOptions = {},
|
|
57
|
+
) => {
|
|
58
|
+
const cacheSessionId = getOpenAIResponsesCacheSessionId(options);
|
|
59
|
+
|
|
60
|
+
const xaiHeaders: Record<string, string> = { ...options?.headers };
|
|
61
|
+
if (cacheSessionId) {
|
|
62
|
+
xaiHeaders["x-grok-conv-id"] = cacheSessionId;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const xaiBody: Record<string, unknown> = { ...(options?.extraBody ?? {}) };
|
|
66
|
+
if (cacheSessionId) {
|
|
67
|
+
xaiBody.prompt_cache_key = cacheSessionId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const xaiOptions: OpenAIResponsesOptions = {
|
|
71
|
+
...options,
|
|
72
|
+
headers: xaiHeaders,
|
|
73
|
+
extraBody: xaiBody,
|
|
74
|
+
includeEncryptedReasoning: false,
|
|
75
|
+
filterReasoningHistory: true,
|
|
76
|
+
// Caller-passed value always wins (escape hatch for future xAI behavior
|
|
77
|
+
// changes); otherwise gate the effort dial on the allowlist.
|
|
78
|
+
omitReasoningEffort: options?.omitReasoningEffort ?? !grokSupportsReasoningEffort(model.id),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return streamOpenAIResponses(model, context, xaiOptions);
|
|
82
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limit reason classification and backoff calculation utilities.
|
|
3
|
+
* Ported from opencode-antigravity-auth plugin for consistency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RateLimitReason =
|
|
7
|
+
| "QUOTA_EXHAUSTED"
|
|
8
|
+
| "RATE_LIMIT_EXCEEDED"
|
|
9
|
+
| "MODEL_CAPACITY_EXHAUSTED"
|
|
10
|
+
| "SERVER_ERROR"
|
|
11
|
+
| "UNKNOWN";
|
|
12
|
+
|
|
13
|
+
const QUOTA_EXHAUSTED_BACKOFF_MS = 30 * 60 * 1000; // 30 min
|
|
14
|
+
const RATE_LIMIT_EXCEEDED_BACKOFF_MS = 30 * 1000; // 30s
|
|
15
|
+
const MODEL_CAPACITY_BASE_MS = 45 * 1000; // 45s base
|
|
16
|
+
const MODEL_CAPACITY_JITTER_MS = 30 * 1000; // ±15s
|
|
17
|
+
const SERVER_ERROR_BACKOFF_MS = 20 * 1000; // 20s
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classify a rate-limit error message into a reason category.
|
|
21
|
+
* Priority order: MODEL_CAPACITY > RATE_LIMIT > QUOTA > SERVER_ERROR > UNKNOWN.
|
|
22
|
+
*
|
|
23
|
+
* "resource exhausted" maps to MODEL_CAPACITY (transient, short wait)
|
|
24
|
+
* "quota exceeded" maps to QUOTA_EXHAUSTED (long wait, switch account)
|
|
25
|
+
*/
|
|
26
|
+
export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
27
|
+
const lower = errorMessage.toLowerCase();
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
lower.includes("capacity") ||
|
|
31
|
+
lower.includes("overloaded") ||
|
|
32
|
+
lower.includes("529") ||
|
|
33
|
+
lower.includes("503") ||
|
|
34
|
+
lower.includes("resource exhausted")
|
|
35
|
+
) {
|
|
36
|
+
return "MODEL_CAPACITY_EXHAUSTED";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
lower.includes("per minute") ||
|
|
41
|
+
lower.includes("rate limit") ||
|
|
42
|
+
lower.includes("too many requests") ||
|
|
43
|
+
lower.includes("presque")
|
|
44
|
+
) {
|
|
45
|
+
return "RATE_LIMIT_EXCEEDED";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (lower.includes("exhausted") || lower.includes("quota") || lower.includes("usage limit")) {
|
|
49
|
+
return "QUOTA_EXHAUSTED";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (lower.includes("500") || lower.includes("internal error") || lower.includes("internal server error")) {
|
|
53
|
+
return "SERVER_ERROR";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return "UNKNOWN";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate backoff delay in ms for a given rate limit reason.
|
|
61
|
+
* MODEL_CAPACITY gets jitter to prevent thundering herd.
|
|
62
|
+
*/
|
|
63
|
+
export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
64
|
+
switch (reason) {
|
|
65
|
+
case "QUOTA_EXHAUSTED":
|
|
66
|
+
return QUOTA_EXHAUSTED_BACKOFF_MS;
|
|
67
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
68
|
+
return RATE_LIMIT_EXCEEDED_BACKOFF_MS;
|
|
69
|
+
case "MODEL_CAPACITY_EXHAUSTED":
|
|
70
|
+
return MODEL_CAPACITY_BASE_MS + Math.random() * MODEL_CAPACITY_JITTER_MS;
|
|
71
|
+
case "SERVER_ERROR":
|
|
72
|
+
return SERVER_ERROR_BACKOFF_MS;
|
|
73
|
+
default:
|
|
74
|
+
return QUOTA_EXHAUSTED_BACKOFF_MS; // conservative default
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
79
|
+
const USAGE_LIMIT_PATTERN =
|
|
80
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|resource.?exhausted/i;
|
|
81
|
+
|
|
82
|
+
export function isUsageLimitError(errorMessage: string): boolean {
|
|
83
|
+
return USAGE_LIMIT_PATTERN.test(errorMessage);
|
|
84
|
+
}
|