@aryee337/aery-ai 0.2.28 → 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,267 @@
|
|
|
1
|
+
import { $env } from "@aryee337/aery-utils";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
|
+
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
5
|
+
|
|
6
|
+
function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
|
|
7
|
+
if (value === undefined) return fallback;
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
10
|
+
if (parsed <= 0) return undefined;
|
|
11
|
+
return Math.trunc(parsed);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the idle timeout used for provider streaming transports.
|
|
16
|
+
*
|
|
17
|
+
* `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is accepted as a backward-compatible alias.
|
|
18
|
+
* Set `PI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
|
|
19
|
+
*
|
|
20
|
+
* Providers that legitimately stream much slower than the global default can pass
|
|
21
|
+
* `fallbackMs` to widen the floor used when neither env var nor caller option is set.
|
|
22
|
+
* Caller options still take precedence; env overrides still trump the fallback.
|
|
23
|
+
*/
|
|
24
|
+
export function getStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
|
|
25
|
+
return normalizeIdleTimeoutMs($env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the idle timeout used for OpenAI-family streaming transports.
|
|
30
|
+
*
|
|
31
|
+
* `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` takes precedence over the generic
|
|
32
|
+
* `PI_STREAM_IDLE_TIMEOUT_MS` because some deployments tune OpenAI-compatible
|
|
33
|
+
* backends separately from Anthropic/Gemini-style transports.
|
|
34
|
+
*
|
|
35
|
+
* Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
|
|
36
|
+
*/
|
|
37
|
+
export function getOpenAIStreamIdleTimeoutMs(fallbackMs: number = DEFAULT_STREAM_IDLE_TIMEOUT_MS): number | undefined {
|
|
38
|
+
return normalizeIdleTimeoutMs($env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS, fallbackMs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the timeout used while waiting for the first stream event.
|
|
43
|
+
* The first token can legitimately take longer than later inter-event gaps,
|
|
44
|
+
* so the default never undershoots the steady-state idle timeout.
|
|
45
|
+
*
|
|
46
|
+
* Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog.
|
|
47
|
+
*
|
|
48
|
+
* Providers whose first response can legitimately take longer (heavy reasoning,
|
|
49
|
+
* slow cold-start proxies) can pass `fallbackMs` to widen the floor used when
|
|
50
|
+
* neither env var nor caller option is set. Caller options still take precedence;
|
|
51
|
+
* env overrides still trump the fallback.
|
|
52
|
+
*/
|
|
53
|
+
export function getStreamFirstEventTimeoutMs(
|
|
54
|
+
idleTimeoutMs?: number,
|
|
55
|
+
fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
|
|
56
|
+
): number | undefined {
|
|
57
|
+
const fallback = idleTimeoutMs === undefined ? fallbackMs : Math.max(fallbackMs, idleTimeoutMs);
|
|
58
|
+
return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the first-event timeout used for OpenAI-family streaming transports.
|
|
63
|
+
*
|
|
64
|
+
* Precedence: explicit `PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS` (including a
|
|
65
|
+
* `"0"` disable) wins outright. Otherwise the resolved idle (caller-supplied
|
|
66
|
+
* `idleTimeoutMs` — which itself already encompasses per-call
|
|
67
|
+
* `streamIdleTimeoutMs` or `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` resolved
|
|
68
|
+
* upstream) floors the first-event budget so slow local OpenAI-compatible
|
|
69
|
+
* servers are not undercut by a shorter `PI_STREAM_FIRST_EVENT_TIMEOUT_MS`
|
|
70
|
+
* or the global default during prompt processing.
|
|
71
|
+
*
|
|
72
|
+
* Returns `undefined` when an explicit env knob disables the watchdog.
|
|
73
|
+
*/
|
|
74
|
+
export function getOpenAIStreamFirstEventTimeoutMs(
|
|
75
|
+
idleTimeoutMs?: number,
|
|
76
|
+
fallbackMs: number = DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS,
|
|
77
|
+
): number | undefined {
|
|
78
|
+
const openAIFirstEventRaw = $env.PI_OPENAI_STREAM_FIRST_EVENT_TIMEOUT_MS;
|
|
79
|
+
if (openAIFirstEventRaw !== undefined) {
|
|
80
|
+
return normalizeIdleTimeoutMs(openAIFirstEventRaw, fallbackMs);
|
|
81
|
+
}
|
|
82
|
+
const base = normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallbackMs);
|
|
83
|
+
if (base === undefined) return undefined;
|
|
84
|
+
if (idleTimeoutMs === undefined || idleTimeoutMs <= 0) return base;
|
|
85
|
+
return Math.max(base, idleTimeoutMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface IdleTimeoutIteratorOptions {
|
|
89
|
+
idleTimeoutMs?: number;
|
|
90
|
+
firstItemTimeoutMs?: number;
|
|
91
|
+
errorMessage: string;
|
|
92
|
+
firstItemErrorMessage?: string;
|
|
93
|
+
onIdle?: () => void;
|
|
94
|
+
onFirstItemTimeout?: () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Optional semantic-progress predicate. Non-progress items are still yielded,
|
|
97
|
+
* but they do not reset the idle deadline. This prevents provider
|
|
98
|
+
* keepalive/no-op events from keeping a stalled tool call alive forever.
|
|
99
|
+
*/
|
|
100
|
+
isProgressItem?: (item: unknown) => boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Cancel iteration as soon as this signal aborts. Required for caller-driven
|
|
103
|
+
* cancellation (ESC) when the underlying transport does not surface signal
|
|
104
|
+
* aborts to the iterator (HTTP/2 proxies, native sockets, mocked fetch).
|
|
105
|
+
* Without this, the consumer sleeps on iterator.next() until the idle/first
|
|
106
|
+
* -event watchdog fires — observable as the issue #912 "Working… forever"
|
|
107
|
+
* symptom on the github-copilot provider.
|
|
108
|
+
*/
|
|
109
|
+
abortSignal?: AbortSignal;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Yields items from an async iterable while enforcing a maximum idle gap between items.
|
|
114
|
+
*
|
|
115
|
+
* The first item may use a shorter timeout so stuck requests can be aborted and retried
|
|
116
|
+
* before any user-visible content has streamed.
|
|
117
|
+
*/
|
|
118
|
+
export async function* iterateWithIdleTimeout<T>(
|
|
119
|
+
iterable: AsyncIterable<T>,
|
|
120
|
+
options: IdleTimeoutIteratorOptions,
|
|
121
|
+
): AsyncGenerator<T> {
|
|
122
|
+
const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
|
|
123
|
+
const firstItemDeadlineMs =
|
|
124
|
+
firstItemTimeoutMs !== undefined && firstItemTimeoutMs > 0 ? Date.now() + firstItemTimeoutMs : undefined;
|
|
125
|
+
const abortSignal = options.abortSignal;
|
|
126
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
127
|
+
|
|
128
|
+
const closeIterator = (): void => {
|
|
129
|
+
const returnPromise = iterator.return?.();
|
|
130
|
+
if (returnPromise) {
|
|
131
|
+
void returnPromise.catch(() => {});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (abortSignal?.aborted) {
|
|
136
|
+
closeIterator();
|
|
137
|
+
throw abortReason(abortSignal);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const withRacy = <T>(promise: Promise<T>) =>
|
|
141
|
+
promise.then(
|
|
142
|
+
result => ({ kind: "next" as const, result }),
|
|
143
|
+
error => ({ kind: "error" as const, error }),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let awaitingFirstItem = true;
|
|
147
|
+
const markFirstItemReceived = () => {
|
|
148
|
+
awaitingFirstItem = false;
|
|
149
|
+
};
|
|
150
|
+
const isProgressItem = (item: T): boolean => {
|
|
151
|
+
if (!options.isProgressItem) return true;
|
|
152
|
+
try {
|
|
153
|
+
return options.isProgressItem(item);
|
|
154
|
+
} catch {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
let lastProgressAt = Date.now();
|
|
159
|
+
|
|
160
|
+
const noTimeoutEnforced =
|
|
161
|
+
(firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
|
|
162
|
+
(options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0);
|
|
163
|
+
|
|
164
|
+
while (true) {
|
|
165
|
+
let activeTimeoutMs: number | undefined;
|
|
166
|
+
if (awaitingFirstItem) {
|
|
167
|
+
if (firstItemDeadlineMs !== undefined) {
|
|
168
|
+
activeTimeoutMs = firstItemDeadlineMs - Date.now();
|
|
169
|
+
if (activeTimeoutMs <= 0) {
|
|
170
|
+
options.onFirstItemTimeout?.();
|
|
171
|
+
closeIterator();
|
|
172
|
+
throw new Error(options.firstItemErrorMessage ?? options.errorMessage);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else if (options.idleTimeoutMs !== undefined && options.idleTimeoutMs > 0) {
|
|
176
|
+
activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
|
|
177
|
+
if (activeTimeoutMs <= 0) {
|
|
178
|
+
options.onIdle?.();
|
|
179
|
+
closeIterator();
|
|
180
|
+
throw new Error(options.errorMessage);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const nextResultPromise = withRacy(iterator.next());
|
|
185
|
+
|
|
186
|
+
const racers: Array<
|
|
187
|
+
Promise<
|
|
188
|
+
| { kind: "next"; result: IteratorResult<T> }
|
|
189
|
+
| { kind: "error"; error: unknown }
|
|
190
|
+
| { kind: "timeout" }
|
|
191
|
+
| { kind: "abort" }
|
|
192
|
+
>
|
|
193
|
+
> = [nextResultPromise];
|
|
194
|
+
|
|
195
|
+
let timer: NodeJS.Timeout | undefined;
|
|
196
|
+
let resolveTimeout: ((value: { kind: "timeout" }) => void) | undefined;
|
|
197
|
+
const enforceTimeout = !noTimeoutEnforced && activeTimeoutMs !== undefined && activeTimeoutMs > 0;
|
|
198
|
+
if (enforceTimeout) {
|
|
199
|
+
const { promise, resolve } = Promise.withResolvers<{ kind: "timeout" }>();
|
|
200
|
+
resolveTimeout = resolve;
|
|
201
|
+
timer = setTimeout(() => resolve({ kind: "timeout" }), activeTimeoutMs);
|
|
202
|
+
racers.push(promise);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let abortListener: (() => void) | undefined;
|
|
206
|
+
let resolveAbort: ((value: { kind: "abort" }) => void) | undefined;
|
|
207
|
+
if (abortSignal) {
|
|
208
|
+
const { promise, resolve } = Promise.withResolvers<{ kind: "abort" }>();
|
|
209
|
+
resolveAbort = resolve;
|
|
210
|
+
abortListener = () => resolve({ kind: "abort" });
|
|
211
|
+
abortSignal.addEventListener("abort", abortListener, { once: true });
|
|
212
|
+
racers.push(promise);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const outcome = await Promise.race(racers);
|
|
217
|
+
if (outcome.kind === "abort") {
|
|
218
|
+
closeIterator();
|
|
219
|
+
throw abortReason(abortSignal!);
|
|
220
|
+
}
|
|
221
|
+
if (outcome.kind === "timeout") {
|
|
222
|
+
if (!awaitingFirstItem) {
|
|
223
|
+
options.onIdle?.();
|
|
224
|
+
} else {
|
|
225
|
+
options.onFirstItemTimeout?.();
|
|
226
|
+
}
|
|
227
|
+
closeIterator();
|
|
228
|
+
throw new Error(
|
|
229
|
+
!awaitingFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (outcome.kind === "error") {
|
|
233
|
+
throw outcome.error;
|
|
234
|
+
}
|
|
235
|
+
if (outcome.result.done) {
|
|
236
|
+
markFirstItemReceived();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const item = outcome.result.value;
|
|
240
|
+
// Non-progress items (e.g. provider keepalives, synthetic `start` events that
|
|
241
|
+
// arrive before the model has produced any tokens) MUST NOT flip us out of
|
|
242
|
+
// `awaitingFirstItem`. Otherwise the next iteration switches from the (longer)
|
|
243
|
+
// first-item watchdog to the (shorter) idle watchdog while we're still waiting
|
|
244
|
+
// on the model's first real output.
|
|
245
|
+
if (isProgressItem(item)) {
|
|
246
|
+
markFirstItemReceived();
|
|
247
|
+
lastProgressAt = Date.now();
|
|
248
|
+
}
|
|
249
|
+
yield item;
|
|
250
|
+
} finally {
|
|
251
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
252
|
+
// Resolve dangling promises so the racers don't leak (Promise.race is one-shot).
|
|
253
|
+
resolveTimeout?.({ kind: "timeout" });
|
|
254
|
+
if (abortListener && abortSignal) {
|
|
255
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
256
|
+
}
|
|
257
|
+
resolveAbort?.({ kind: "abort" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function abortReason(signal: AbortSignal): Error {
|
|
263
|
+
const reason = signal.reason;
|
|
264
|
+
if (reason instanceof Error) return reason;
|
|
265
|
+
if (typeof reason === "string") return new Error(reason);
|
|
266
|
+
return new Error("Request was aborted");
|
|
267
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { parse as partialParse } from "partial-json";
|
|
2
|
+
|
|
3
|
+
const QUOTE = 0x22;
|
|
4
|
+
const BACKSLASH = 0x5c;
|
|
5
|
+
const U = 0x75;
|
|
6
|
+
|
|
7
|
+
// Valid chars after `\`: " \ / b f n r t u
|
|
8
|
+
const VALID_ESCAPE_CHAR = new Uint8Array(128);
|
|
9
|
+
for (const ch of '"\\/bfnrtu') VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
|
|
10
|
+
|
|
11
|
+
const CONTROL_ESCAPES: readonly string[] = (() => {
|
|
12
|
+
const e: string[] = [];
|
|
13
|
+
e[0x08] = "\\b";
|
|
14
|
+
e[0x09] = "\\t";
|
|
15
|
+
e[0x0a] = "\\n";
|
|
16
|
+
e[0x0c] = "\\f";
|
|
17
|
+
e[0x0d] = "\\r";
|
|
18
|
+
for (let cp = 0; cp <= 0x1f; cp++) {
|
|
19
|
+
e[cp] ??= `\\u${cp.toString(16).padStart(4, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
return e;
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
function isHexDigit(cp: number): boolean {
|
|
25
|
+
return (cp >= 0x30 && cp <= 0x39) || ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x66);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function repairJson(json: string): string {
|
|
29
|
+
const len = json.length;
|
|
30
|
+
const parts: string[] = [];
|
|
31
|
+
let lastEmit = 0;
|
|
32
|
+
let inString = false;
|
|
33
|
+
let i = 0;
|
|
34
|
+
|
|
35
|
+
while (i < len) {
|
|
36
|
+
if (!inString) {
|
|
37
|
+
// Fast scan: skip to next quote.
|
|
38
|
+
while (i < len && json.charCodeAt(i) !== QUOTE) i++;
|
|
39
|
+
if (i >= len) break;
|
|
40
|
+
inString = true;
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fast scan inside string: advance past chars that need no handling.
|
|
46
|
+
while (i < len) {
|
|
47
|
+
const cp = json.charCodeAt(i);
|
|
48
|
+
if (cp < 0x20 || cp === QUOTE || cp === BACKSLASH) break;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
if (i >= len) break;
|
|
52
|
+
|
|
53
|
+
const cp = json.charCodeAt(i);
|
|
54
|
+
|
|
55
|
+
if (cp === QUOTE) {
|
|
56
|
+
inString = false;
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (cp === BACKSLASH) {
|
|
62
|
+
// Need at least one char after the backslash; treat EOI as invalid escape.
|
|
63
|
+
if (i + 1 >= len) {
|
|
64
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
65
|
+
lastEmit = i + 1;
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nextCp = json.charCodeAt(i + 1);
|
|
71
|
+
|
|
72
|
+
if (nextCp === U) {
|
|
73
|
+
// Need full \uXXXX, all four digits, all hex.
|
|
74
|
+
if (
|
|
75
|
+
i + 5 < len &&
|
|
76
|
+
isHexDigit(json.charCodeAt(i + 2)) &&
|
|
77
|
+
isHexDigit(json.charCodeAt(i + 3)) &&
|
|
78
|
+
isHexDigit(json.charCodeAt(i + 4)) &&
|
|
79
|
+
isHexDigit(json.charCodeAt(i + 5))
|
|
80
|
+
) {
|
|
81
|
+
i += 6;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Truncated or non-hex \u — escape the backslash, re-process the rest.
|
|
85
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
86
|
+
lastEmit = i + 1;
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (nextCp < 128 && VALID_ESCAPE_CHAR[nextCp] === 1) {
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
parts.push(json.slice(lastEmit, i), "\\\\");
|
|
97
|
+
lastEmit = i + 1;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Control character (cp < 0x20).
|
|
103
|
+
parts.push(json.slice(lastEmit, i), CONTROL_ESCAPES[cp]);
|
|
104
|
+
lastEmit = i + 1;
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!parts.length) return json;
|
|
109
|
+
if (lastEmit < len) parts.push(json.slice(lastEmit));
|
|
110
|
+
return parts.join("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseJsonWithRepair<T>(json: string): T {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(json) as T;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const repairedJson = repairJson(json);
|
|
118
|
+
if (repairedJson !== json) {
|
|
119
|
+
return JSON.parse(repairedJson) as T;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Attempts to parse potentially incomplete JSON during streaming.
|
|
127
|
+
* Always returns a valid object, even if the JSON is incomplete.
|
|
128
|
+
*
|
|
129
|
+
* @param partialJson The partial JSON string from streaming
|
|
130
|
+
* @returns Parsed object or empty object if parsing fails
|
|
131
|
+
*/
|
|
132
|
+
export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
|
|
133
|
+
partialJson = partialJson?.trimStart();
|
|
134
|
+
if (!partialJson) {
|
|
135
|
+
return {} as T;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(partialJson) as T;
|
|
139
|
+
} catch {
|
|
140
|
+
partialJson = repairJson(partialJson);
|
|
141
|
+
try {
|
|
142
|
+
return (partialParse(partialJson) ?? {}) as T;
|
|
143
|
+
} catch {
|
|
144
|
+
// If all parsing fails, return empty object
|
|
145
|
+
return {} as T;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
152
|
+
* re-parse a streaming tool-call argument buffer. Bounds the mid-stream
|
|
153
|
+
* partial-parse cost from quadratic to linear in N.
|
|
154
|
+
*/
|
|
155
|
+
export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
|
|
159
|
+
*
|
|
160
|
+
* Tool calls arrive as a long sequence of small deltas — calling
|
|
161
|
+
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
162
|
+
* each time, giving O(N²) work in the total buffer length. Throttling skips
|
|
163
|
+
* the re-parse until at least `minGrowthBytes` of new content has arrived
|
|
164
|
+
* since the last successful parse, bounding mid-stream cost to O(N).
|
|
165
|
+
*
|
|
166
|
+
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
167
|
+
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
168
|
+
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
169
|
+
* updates by at most `minGrowthBytes` of accumulated partial content.
|
|
170
|
+
*
|
|
171
|
+
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
172
|
+
* when the buffer has not grown enough to warrant a re-parse.
|
|
173
|
+
*/
|
|
174
|
+
export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
|
|
175
|
+
partialJson: string | undefined,
|
|
176
|
+
lastParsedLen: number,
|
|
177
|
+
minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
|
|
178
|
+
): { value: T; parsedLen: number } | null {
|
|
179
|
+
const len = partialJson?.length ?? 0;
|
|
180
|
+
if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
|
|
181
|
+
return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
|
|
182
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { isXAIAccessTokenExpiring, refreshXAIOAuthToken, validateXAIEndpoint, XAIOAuthFlow } from "../xai-oauth";
|
|
3
|
+
|
|
4
|
+
const originalFetch = global.fetch;
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
global.fetch = originalFetch;
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function jwtWithExp(exp: number): string {
|
|
12
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
13
|
+
const payload = Buffer.from(JSON.stringify({ exp })).toString("base64url");
|
|
14
|
+
return `${header}.${payload}.sig`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("isXAIAccessTokenExpiring", () => {
|
|
18
|
+
it("returns false for an empty string", () => {
|
|
19
|
+
expect(isXAIAccessTokenExpiring("")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns false for a non-JWT", () => {
|
|
23
|
+
expect(isXAIAccessTokenExpiring("not.a.jwt")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns true when exp is already in the past", () => {
|
|
27
|
+
const now = Math.floor(Date.now() / 1000);
|
|
28
|
+
expect(isXAIAccessTokenExpiring(jwtWithExp(now - 60))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns false when exp is well in the future", () => {
|
|
32
|
+
const now = Math.floor(Date.now() / 1000);
|
|
33
|
+
expect(isXAIAccessTokenExpiring(jwtWithExp(now + 3600))).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("validateXAIEndpoint", () => {
|
|
38
|
+
it("rejects non-HTTPS URLs", () => {
|
|
39
|
+
expect(() => validateXAIEndpoint("http://x.ai/token", "token_endpoint")).toThrow(/Invalid xAI token_endpoint/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects non-xAI hosts", () => {
|
|
43
|
+
expect(() => validateXAIEndpoint("https://evil.com/token", "token_endpoint")).toThrow(
|
|
44
|
+
/Invalid xAI token_endpoint/,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("accepts the x.ai apex and *.x.ai subdomains", () => {
|
|
49
|
+
expect(validateXAIEndpoint("https://x.ai/token", "token_endpoint")).toBe("https://x.ai/token");
|
|
50
|
+
expect(validateXAIEndpoint("https://auth.x.ai/oauth/token", "token_endpoint")).toBe(
|
|
51
|
+
"https://auth.x.ai/oauth/token",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("refreshXAIOAuthToken", () => {
|
|
57
|
+
it("rejects an empty refresh_token without making a network call", async () => {
|
|
58
|
+
const fetchMock = vi.fn(async () => {
|
|
59
|
+
throw new Error("fetch should not be called when refresh_token is empty");
|
|
60
|
+
});
|
|
61
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
62
|
+
|
|
63
|
+
await expect(refreshXAIOAuthToken("")).rejects.toThrow(/missing refresh_token/);
|
|
64
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("XAIOAuthFlow", () => {
|
|
69
|
+
it("pins the redirect URI to xAI's allowlisted loopback port", () => {
|
|
70
|
+
const flow = new XAIOAuthFlow({});
|
|
71
|
+
|
|
72
|
+
expect(flow.redirectUri).toBe("http://127.0.0.1:56121/callback");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("XAIOAuthFlow.exchangeToken", () => {
|
|
77
|
+
it("rejects when the token-exchange response is missing access_token", async () => {
|
|
78
|
+
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
79
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
80
|
+
if (url.includes("/.well-known/openid-configuration")) {
|
|
81
|
+
return new Response(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
authorization_endpoint: "https://auth.x.ai/oauth/authorize",
|
|
84
|
+
token_endpoint: "https://auth.x.ai/oauth/token",
|
|
85
|
+
}),
|
|
86
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
// Token-exchange response deliberately omits `access_token` to exercise
|
|
90
|
+
// the missing-token rejection path. The value of `refresh_token` here is
|
|
91
|
+
// a literal test marker, not a real secret — the test verifies
|
|
92
|
+
// exchangeToken throws before any token would be persisted.
|
|
93
|
+
return new Response(JSON.stringify({ refresh_token: "stub-refresh-token-for-test-only" }), {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
99
|
+
|
|
100
|
+
const flow = new XAIOAuthFlow({});
|
|
101
|
+
await flow.generateAuthUrl("state-abc", "http://127.0.0.1:56121/callback");
|
|
102
|
+
|
|
103
|
+
await expect(flow.exchangeToken("code-xyz", "state-abc", "http://127.0.0.1:56121/callback")).rejects.toThrow(
|
|
104
|
+
/access_token/,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alibaba Coding Plan login flow.
|
|
3
|
+
*
|
|
4
|
+
* Alibaba Coding Plan provides OpenAI-compatible models via https://coding-intl.dashscope.aliyuncs.com/v1.
|
|
5
|
+
*
|
|
6
|
+
* This is not OAuth - it's a simple API key flow:
|
|
7
|
+
* 1. Open browser to Alibaba Cloud DashScope API key settings
|
|
8
|
+
* 2. User copies their API key
|
|
9
|
+
* 3. User pastes the API key into the CLI
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { validateOpenAICompatibleApiKey } from "./api-key-validation";
|
|
13
|
+
import type { OAuthController } from "./types";
|
|
14
|
+
|
|
15
|
+
const AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
|
|
16
|
+
const API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
|
|
17
|
+
const VALIDATION_MODEL = "qwen3.5-plus";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Login to Alibaba Coding Plan.
|
|
21
|
+
*
|
|
22
|
+
* Opens browser to API keys page, prompts user to paste their API key.
|
|
23
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
24
|
+
*/
|
|
25
|
+
export async function loginAlibabaCodingPlan(options: OAuthController): Promise<string> {
|
|
26
|
+
if (!options.onPrompt) {
|
|
27
|
+
throw new Error("Alibaba Coding Plan login requires onPrompt callback");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
options.onAuth?.({
|
|
31
|
+
url: AUTH_URL,
|
|
32
|
+
instructions: "Copy your API key from the Alibaba Cloud DashScope console",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const apiKey = await options.onPrompt({
|
|
36
|
+
message: "Paste your Alibaba Coding Plan API key",
|
|
37
|
+
placeholder: "sk-...",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (options.signal?.aborted) {
|
|
41
|
+
throw new Error("Login cancelled");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const trimmed = apiKey.trim();
|
|
45
|
+
if (!trimmed) {
|
|
46
|
+
throw new Error("API key is required");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
options.onProgress?.("Validating API key...");
|
|
50
|
+
await validateOpenAICompatibleApiKey({
|
|
51
|
+
provider: "Alibaba Coding Plan",
|
|
52
|
+
apiKey: trimmed,
|
|
53
|
+
baseUrl: API_BASE_URL,
|
|
54
|
+
model: VALIDATION_MODEL,
|
|
55
|
+
signal: options.signal,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|