@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive rewrite for nodes that look like `JSON.stringify(zodSchemaInstance)`
|
|
3
|
+
* output rather than JSON Schema. MCP servers using Zod 4 sometimes ship a
|
|
4
|
+
* serialised schema instance directly as a tool's `inputSchema`, because the
|
|
5
|
+
* fields Zod surfaces on its instances (`type`, `enum`, `options`, `def`) shadow
|
|
6
|
+
* (and clash with) JSON Schema keywords. The resulting payload is neither valid
|
|
7
|
+
* Zod nor valid JSON Schema 2020-12 and Anthropic's strict validator rejects
|
|
8
|
+
* the whole tool list.
|
|
9
|
+
*
|
|
10
|
+
* Symptoms we've observed (gitnexus_impact.direction):
|
|
11
|
+
* {
|
|
12
|
+
* def: { type: "enum", entries: { upstream: "upstream", ... } },
|
|
13
|
+
* type: "enum", // <- invalid `type` value
|
|
14
|
+
* enum: { upstream: "upstream", ... }, // <- `enum` MUST be an array
|
|
15
|
+
* options: ["upstream", "downstream"],
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* This module recognises the shape (`def.type === node.type` and `def.type` is
|
|
19
|
+
* a known Zod kind) and rewrites it to clean JSON Schema where deterministic.
|
|
20
|
+
* For Zod kinds we don't fully model, we strip the toxic siblings (`def`,
|
|
21
|
+
* `options`, object-shaped `enum`) and drop an invalid `type` so the remainder
|
|
22
|
+
* passes meta-schema validation as a permissive node.
|
|
23
|
+
*
|
|
24
|
+
* Pure / identity-preserving: returns the input reference when nothing changes.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isJsonObject, type JsonObject } from "./types";
|
|
28
|
+
|
|
29
|
+
const VALID_JSON_SCHEMA_TYPES: Record<string, true> = {
|
|
30
|
+
string: true,
|
|
31
|
+
number: true,
|
|
32
|
+
integer: true,
|
|
33
|
+
boolean: true,
|
|
34
|
+
object: true,
|
|
35
|
+
array: true,
|
|
36
|
+
null: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Known Zod 4 schema kinds as surfaced on `_def.type` / `.type`. Matching this
|
|
41
|
+
* set (rather than just "has `def`") is what keeps us from rewriting legitimate
|
|
42
|
+
* JSON Schemas that happen to use `def` as a property name.
|
|
43
|
+
*/
|
|
44
|
+
const ZOD_KINDS: Record<string, true> = {
|
|
45
|
+
string: true,
|
|
46
|
+
number: true,
|
|
47
|
+
int: true,
|
|
48
|
+
boolean: true,
|
|
49
|
+
bigint: true,
|
|
50
|
+
null: true,
|
|
51
|
+
undefined: true,
|
|
52
|
+
void: true,
|
|
53
|
+
any: true,
|
|
54
|
+
unknown: true,
|
|
55
|
+
never: true,
|
|
56
|
+
date: true,
|
|
57
|
+
symbol: true,
|
|
58
|
+
nan: true,
|
|
59
|
+
enum: true,
|
|
60
|
+
literal: true,
|
|
61
|
+
object: true,
|
|
62
|
+
array: true,
|
|
63
|
+
tuple: true,
|
|
64
|
+
record: true,
|
|
65
|
+
map: true,
|
|
66
|
+
set: true,
|
|
67
|
+
union: true,
|
|
68
|
+
discriminatedUnion: true,
|
|
69
|
+
intersection: true,
|
|
70
|
+
lazy: true,
|
|
71
|
+
promise: true,
|
|
72
|
+
function: true,
|
|
73
|
+
file: true,
|
|
74
|
+
custom: true,
|
|
75
|
+
template_literal: true,
|
|
76
|
+
optional: true,
|
|
77
|
+
nullable: true,
|
|
78
|
+
default: true,
|
|
79
|
+
prefault: true,
|
|
80
|
+
catch: true,
|
|
81
|
+
pipe: true,
|
|
82
|
+
transform: true,
|
|
83
|
+
brand: true,
|
|
84
|
+
readonly: true,
|
|
85
|
+
success: true,
|
|
86
|
+
nonoptional: true,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ZOD_SCALAR_TO_JSON_TYPE: Record<string, string> = {
|
|
90
|
+
string: "string",
|
|
91
|
+
number: "number",
|
|
92
|
+
int: "integer",
|
|
93
|
+
boolean: "boolean",
|
|
94
|
+
null: "null",
|
|
95
|
+
bigint: "string",
|
|
96
|
+
date: "string",
|
|
97
|
+
nan: "number",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const ZOD_NOISE_KEYS: Record<string, true> = {
|
|
101
|
+
def: true,
|
|
102
|
+
options: true,
|
|
103
|
+
_zod: true,
|
|
104
|
+
checks: true,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* JSON Schema keywords where `null` is a legal value (literal payload positions).
|
|
109
|
+
* Anywhere else, a `null`-valued key is a meta-schema violation — Zod scalars
|
|
110
|
+
* leak `format: null`, `minLength: null`, etc. that we have to scrub.
|
|
111
|
+
*/
|
|
112
|
+
const KEYS_THAT_ACCEPT_NULL: Record<string, true> = {
|
|
113
|
+
default: true,
|
|
114
|
+
const: true,
|
|
115
|
+
examples: true,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function isZodLeak(node: JsonObject): boolean {
|
|
119
|
+
const def = node.def;
|
|
120
|
+
if (!isJsonObject(def)) return false;
|
|
121
|
+
const defType = def.type;
|
|
122
|
+
if (typeof defType !== "string" || !ZOD_KINDS[defType]) return false;
|
|
123
|
+
// Both surface and inner `.type` must agree — Zod always mirrors `_def.type`
|
|
124
|
+
// onto the instance, so this is a near-zero false-positive guard.
|
|
125
|
+
return node.type === defType;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inferTypeFromValues(values: readonly unknown[]): string {
|
|
129
|
+
if (values.length === 0) return "string";
|
|
130
|
+
const first = values[0];
|
|
131
|
+
if (typeof first === "number") return Number.isInteger(first) ? "integer" : "number";
|
|
132
|
+
if (typeof first === "boolean") return "boolean";
|
|
133
|
+
if (first === null) return "null";
|
|
134
|
+
return "string";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function unwrapInnerSchema(def: JsonObject): unknown {
|
|
138
|
+
// Zod uses different fields depending on the wrapper:
|
|
139
|
+
// optional/nullable/readonly/brand/default → `innerType`
|
|
140
|
+
// pipe → `in` (or `out`)
|
|
141
|
+
// lazy → `getter` (a function — gone after JSON.stringify); fall back to {}
|
|
142
|
+
return def.innerType ?? def.in ?? def.out ?? def.schema ?? def.element ?? {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function copyWithoutNoise(node: JsonObject): JsonObject {
|
|
146
|
+
const out: JsonObject = {};
|
|
147
|
+
for (const key in node) {
|
|
148
|
+
if (ZOD_NOISE_KEYS[key]) continue;
|
|
149
|
+
const value = node[key];
|
|
150
|
+
if (value === null && !KEYS_THAT_ACCEPT_NULL[key]) continue;
|
|
151
|
+
out[key] = value;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rewriteZodNode(node: JsonObject, seen: WeakSet<object>): unknown {
|
|
157
|
+
const def = node.def as JsonObject;
|
|
158
|
+
const kind = def.type as string;
|
|
159
|
+
|
|
160
|
+
switch (kind) {
|
|
161
|
+
case "enum": {
|
|
162
|
+
// Prefer node.options (array form Zod exposes) → def.entries values →
|
|
163
|
+
// object-shaped node.enum values. All three carry the same data.
|
|
164
|
+
const optionsArray = Array.isArray(node.options) ? (node.options as unknown[]) : null;
|
|
165
|
+
const entries = isJsonObject(def.entries) ? Object.values(def.entries) : null;
|
|
166
|
+
const enumObj = isJsonObject(node.enum) ? Object.values(node.enum) : null;
|
|
167
|
+
const values = optionsArray ?? entries ?? enumObj ?? [];
|
|
168
|
+
return { type: inferTypeFromValues(values), enum: values };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "literal": {
|
|
172
|
+
const values = Array.isArray(def.values) ? (def.values as unknown[]) : [];
|
|
173
|
+
if (values.length === 1) {
|
|
174
|
+
return { const: values[0] };
|
|
175
|
+
}
|
|
176
|
+
if (values.length > 1) {
|
|
177
|
+
return { type: inferTypeFromValues(values), enum: values };
|
|
178
|
+
}
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case "union":
|
|
183
|
+
case "discriminatedUnion": {
|
|
184
|
+
const arms = Array.isArray(def.options)
|
|
185
|
+
? (def.options as unknown[])
|
|
186
|
+
: Array.isArray(node.options)
|
|
187
|
+
? (node.options as unknown[])
|
|
188
|
+
: [];
|
|
189
|
+
return { anyOf: arms.map(x => walk(x, seen)) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case "intersection": {
|
|
193
|
+
return {
|
|
194
|
+
allOf: [walk(def.left, seen), walk(def.right, seen)],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case "array": {
|
|
199
|
+
return { type: "array", items: walk(def.element, seen) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "set": {
|
|
203
|
+
const element = def.valueType ?? def.element;
|
|
204
|
+
return { type: "array", uniqueItems: true, items: walk(element, seen) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case "tuple": {
|
|
208
|
+
const items = Array.isArray(def.items) ? (def.items as unknown[]) : [];
|
|
209
|
+
const out: JsonObject = { type: "array", prefixItems: items.map(x => walk(x, seen)) };
|
|
210
|
+
const rest = def.rest;
|
|
211
|
+
if (rest != null) out.items = walk(rest, seen);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case "record":
|
|
216
|
+
case "map": {
|
|
217
|
+
return { type: "object", additionalProperties: walk(def.valueType, seen) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case "object": {
|
|
221
|
+
const shape = isJsonObject(def.shape) ? def.shape : ({} as JsonObject);
|
|
222
|
+
const properties: JsonObject = {};
|
|
223
|
+
const required: string[] = [];
|
|
224
|
+
for (const key in shape) {
|
|
225
|
+
const inner = walk(shape[key], seen);
|
|
226
|
+
properties[key] = inner;
|
|
227
|
+
if (!isOptionalEntry(shape[key])) required.push(key);
|
|
228
|
+
}
|
|
229
|
+
const out: JsonObject = { type: "object", properties };
|
|
230
|
+
if (required.length > 0) out.required = required;
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "nonoptional":
|
|
235
|
+
case "optional":
|
|
236
|
+
case "nullable":
|
|
237
|
+
case "default":
|
|
238
|
+
case "prefault":
|
|
239
|
+
case "catch":
|
|
240
|
+
case "readonly":
|
|
241
|
+
case "brand":
|
|
242
|
+
case "lazy":
|
|
243
|
+
case "pipe":
|
|
244
|
+
case "transform": {
|
|
245
|
+
const inner = walk(unwrapInnerSchema(def), seen);
|
|
246
|
+
if (kind === "nullable" && isJsonObject(inner)) {
|
|
247
|
+
if (typeof inner.type === "string") {
|
|
248
|
+
return { ...inner, type: [inner.type, "null"] };
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(inner.type)) {
|
|
251
|
+
return (inner.type as string[]).includes("null")
|
|
252
|
+
? inner
|
|
253
|
+
: { ...inner, type: [...(inner.type as string[]), "null"] };
|
|
254
|
+
}
|
|
255
|
+
// anyOf / allOf / $ref shapes — no scalar `type` field
|
|
256
|
+
return { anyOf: [inner, { type: "null" }] };
|
|
257
|
+
}
|
|
258
|
+
return inner;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
default: {
|
|
262
|
+
// Best-effort: drop the noise, map the kind to a JSON Schema type if
|
|
263
|
+
// we know one, otherwise drop `type` so the node validates as
|
|
264
|
+
// permissive.
|
|
265
|
+
const cleaned = copyWithoutNoise(node);
|
|
266
|
+
const mapped = ZOD_SCALAR_TO_JSON_TYPE[kind];
|
|
267
|
+
if (mapped) {
|
|
268
|
+
cleaned.type = mapped;
|
|
269
|
+
} else if (typeof cleaned.type === "string" && !VALID_JSON_SCHEMA_TYPES[cleaned.type]) {
|
|
270
|
+
delete cleaned.type;
|
|
271
|
+
}
|
|
272
|
+
// Object-shaped `enum` survives as a noise field — remove if present.
|
|
273
|
+
if (cleaned.enum !== undefined && !Array.isArray(cleaned.enum)) {
|
|
274
|
+
delete cleaned.enum;
|
|
275
|
+
}
|
|
276
|
+
return cleaned;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isOptionalEntry(value: unknown): boolean {
|
|
282
|
+
if (!isJsonObject(value)) return false;
|
|
283
|
+
if (!isZodLeak(value)) return false;
|
|
284
|
+
const kind = (value.def as JsonObject).type;
|
|
285
|
+
return kind === "optional" || kind === "default" || kind === "prefault";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Walks a JSON value and rewrites every Zod-instance-shaped node into clean
|
|
290
|
+
* JSON Schema 2020-12. Identity-preserving when no rewrite fires. Tolerates
|
|
291
|
+
* self-referential graphs — a revisited node returns as-is.
|
|
292
|
+
*/
|
|
293
|
+
export function decontaminateZodInstance(value: unknown): unknown {
|
|
294
|
+
return walk(value, new WeakSet());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function walk(value: unknown, seen: WeakSet<object>): unknown {
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
if (seen.has(value)) return value;
|
|
300
|
+
seen.add(value);
|
|
301
|
+
let changed = false;
|
|
302
|
+
const out = value.map(entry => {
|
|
303
|
+
const rewritten = walk(entry, seen);
|
|
304
|
+
if (rewritten !== entry) changed = true;
|
|
305
|
+
return rewritten;
|
|
306
|
+
});
|
|
307
|
+
return changed ? out : value;
|
|
308
|
+
}
|
|
309
|
+
if (!isJsonObject(value)) return value;
|
|
310
|
+
if (seen.has(value)) return value;
|
|
311
|
+
seen.add(value);
|
|
312
|
+
|
|
313
|
+
if (isZodLeak(value)) {
|
|
314
|
+
// Rewrite the node itself, then recurse into the rewrite so any nested
|
|
315
|
+
// Zod-instance children get cleaned in the same pass.
|
|
316
|
+
const rewritten = rewriteZodNode(value, seen);
|
|
317
|
+
return rewritten === value ? value : walk(rewritten, seen);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Plain JSON Schema node: recurse into children, preserving identity when
|
|
321
|
+
// nothing under us changed.
|
|
322
|
+
let changed = false;
|
|
323
|
+
const out: JsonObject = {};
|
|
324
|
+
for (const key in value) {
|
|
325
|
+
const child = value[key];
|
|
326
|
+
const rewritten = walk(child, seen);
|
|
327
|
+
if (rewritten !== child) changed = true;
|
|
328
|
+
out[key] = rewritten;
|
|
329
|
+
}
|
|
330
|
+
return changed ? out : value;
|
|
331
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for mapping `StreamOptions.streamFirstEventTimeoutMs` onto
|
|
3
|
+
* underlying SDK request-timeout options.
|
|
4
|
+
*
|
|
5
|
+
* The hint is intentionally not a watchdog — it just narrows the SDK's
|
|
6
|
+
* "transport timeout" window so a stuck pre-stream request fails fast
|
|
7
|
+
* instead of hanging on the default (often multi-minute) SDK timeout. Once
|
|
8
|
+
* the stream actually starts, silence is not failure; callers must abort
|
|
9
|
+
* to interrupt a quiet stream.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Coerce a caller-supplied `streamFirstEventTimeoutMs` into a positive integer suitable
|
|
14
|
+
* for the SDK's `timeout` option. Returns `undefined` when the caller passed nothing,
|
|
15
|
+
* a non-finite value, or a non-positive value (preserving the SDK's default).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveSdkTimeoutMs(streamFirstEventTimeoutMs: number | undefined): number | undefined {
|
|
18
|
+
if (streamFirstEventTimeoutMs === undefined) return undefined;
|
|
19
|
+
if (!Number.isFinite(streamFirstEventTimeoutMs)) return undefined;
|
|
20
|
+
if (streamFirstEventTimeoutMs <= 0) return undefined;
|
|
21
|
+
return Math.trunc(streamFirstEventTimeoutMs);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build per-request SDK options that combine an abort signal with the optional
|
|
26
|
+
* `streamFirstEventTimeoutMs` request-timeout hint.
|
|
27
|
+
*
|
|
28
|
+
* The returned `{ signal, timeout?, maxRetries? }` shape is compatible with both
|
|
29
|
+
* OpenAI's and Anthropic's `RequestOptions` (and any other SDK that follows the
|
|
30
|
+
* Stainless conventions), so callers from any of those providers can spread the
|
|
31
|
+
* result directly into `client.X.create(params, requestOptions)`.
|
|
32
|
+
*
|
|
33
|
+
* When the hint is set, retries are forced to zero so the SDK does not silently
|
|
34
|
+
* extend the caller's explicit deadline by re-attempting after a timeout.
|
|
35
|
+
*/
|
|
36
|
+
export function createSdkStreamRequestOptions(
|
|
37
|
+
signal: AbortSignal,
|
|
38
|
+
streamFirstEventTimeoutMs: number | undefined,
|
|
39
|
+
): { signal: AbortSignal; timeout?: number; maxRetries?: number } {
|
|
40
|
+
const timeout = resolveSdkTimeoutMs(streamFirstEventTimeoutMs);
|
|
41
|
+
if (timeout === undefined) return { signal };
|
|
42
|
+
return { signal, timeout, maxRetries: 0 };
|
|
43
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { ServerSentEvent } from "@aryee337/aery-utils";
|
|
2
|
+
import type { RawSseEvent } from "../types";
|
|
3
|
+
|
|
4
|
+
type FetchFunction = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
5
|
+
type FetchWithPreconnect = FetchFunction & { preconnect?: typeof fetch.preconnect };
|
|
6
|
+
|
|
7
|
+
type RawSseObserver = (event: RawSseEvent) => void;
|
|
8
|
+
|
|
9
|
+
export function notifyRawSseEvent(observer: RawSseObserver | undefined, event: ServerSentEvent | RawSseEvent): void {
|
|
10
|
+
if (!observer) return;
|
|
11
|
+
try {
|
|
12
|
+
// Pass the event through without cloning `raw`. The only wired observer
|
|
13
|
+
// (`RawSseDebugBuffer.recordEvent`) treats `raw` as owned and never
|
|
14
|
+
// mutates it; new observers must adhere to the same contract.
|
|
15
|
+
// `ServerSentEvent` and `RawSseEvent` are structurally identical
|
|
16
|
+
// (`event: string | null`, `data: string`, `raw: string[]`).
|
|
17
|
+
observer(event as RawSseEvent);
|
|
18
|
+
} catch {
|
|
19
|
+
// Raw stream observers are diagnostic only and must not affect generation.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isSseResponse(response: Response): boolean {
|
|
24
|
+
// `response.body` is non-null for any fetch Response with a body, but we
|
|
25
|
+
// still guard because user-supplied `fetch` mocks may return `{ body: null }`
|
|
26
|
+
// for empty responses and we don't want to wrap those.
|
|
27
|
+
if (!response.ok || !response.body) return false;
|
|
28
|
+
const contentType = response.headers.get("content-type");
|
|
29
|
+
// All providers in this repo emit lowercase `text/event-stream` (verified
|
|
30
|
+
// against anthropic, openai-completions, openai-responses, azure-openai-responses,
|
|
31
|
+
// google-shared, google-gemini-cli, openai-codex-responses, aery-native-client,
|
|
32
|
+
// and the auth-gateway server). A canonical `includes` check is sufficient;
|
|
33
|
+
// if a future provider sends mixed case it will fall back to the unwrapped
|
|
34
|
+
// fetch — observably safe, just no debug tee for that response.
|
|
35
|
+
return contentType?.includes("text/event-stream") ?? false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Reused for every UTF-8 line decode. Safe because lines are split on LF
|
|
39
|
+
// (0x0a), which is single-byte ASCII and never appears inside a UTF-8
|
|
40
|
+
// multi-byte sequence — each line is a complete UTF-8 run, so the decoder
|
|
41
|
+
// carries no state across calls.
|
|
42
|
+
const SSE_LINE_DECODER = new TextDecoder("utf-8");
|
|
43
|
+
|
|
44
|
+
// Decode bytes [start, end) of an SSE line.
|
|
45
|
+
//
|
|
46
|
+
// A previous revision added an ASCII fast-path using `String.fromCharCode.apply`
|
|
47
|
+
// over chunked subarrays, on the theory that skipping `TextDecoder` would save
|
|
48
|
+
// the ~9.7% `decode` self-time the profile reported. In practice the swap
|
|
49
|
+
// *regressed* total wall time: `fromCharCode` became a new 7.8% hotspot,
|
|
50
|
+
// `Uint8Array` allocations grew 5.3%, and `subarray` rose from 11.5% to 18.3%
|
|
51
|
+
// — net loss of ~10pp. Bun's `TextDecoder.decode` has a fast C++ ASCII path
|
|
52
|
+
// that beats chunked `fromCharCode.apply` for the typical sub-1KB SSE line,
|
|
53
|
+
// so we keep the decoder. The line is bounded by LF (0x0a, single-byte
|
|
54
|
+
// ASCII), so each [start, end) slice is a complete UTF-8 run and the shared
|
|
55
|
+
// stateless decoder is safe to reuse.
|
|
56
|
+
function decodeSseLine(buf: Uint8Array, start: number, end: number): string {
|
|
57
|
+
if (start === 0 && end === buf.length) return SSE_LINE_DECODER.decode(buf);
|
|
58
|
+
return SSE_LINE_DECODER.decode(buf.subarray(start, end));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inline SSE event splitter. Walks the byte stream as it flows through a
|
|
63
|
+
* `TransformStream`, dispatching parsed events to the debug observer while
|
|
64
|
+
* the bytes are forwarded unchanged to the response consumer. Replaces the
|
|
65
|
+
* previous `body.tee()` + `readSseEvents` re-parse pipeline so the byte
|
|
66
|
+
* stream is parsed exactly once when a debug observer is attached.
|
|
67
|
+
*
|
|
68
|
+
* Field parsing intentionally mirrors `readSseEvents` in `@aryee337/aery-utils`
|
|
69
|
+
* (only `event` and `data` are observed; `id`/`retry` ignored; CR stripped
|
|
70
|
+
* before LF dispatch; leading space after `:` trimmed; `data:` lines join
|
|
71
|
+
* with `\n`). Reusing `readSseEvents` directly would require a second stream
|
|
72
|
+
* pipeline, which is exactly what this class avoids.
|
|
73
|
+
*/
|
|
74
|
+
class SseTeeParser {
|
|
75
|
+
#observer: RawSseObserver;
|
|
76
|
+
// Trailing bytes from the previous chunk that did not end with LF.
|
|
77
|
+
#partial: Uint8Array | null = null;
|
|
78
|
+
#event: string | null = null;
|
|
79
|
+
#data: string | null = null;
|
|
80
|
+
#raw: string[] = [];
|
|
81
|
+
|
|
82
|
+
constructor(observer: RawSseObserver) {
|
|
83
|
+
this.#observer = observer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
push(chunk: Uint8Array): void {
|
|
87
|
+
// Carry-forward path: concat the partial line with the new chunk so the
|
|
88
|
+
// LF scan walks a single contiguous buffer. The common case (partial is
|
|
89
|
+
// null) skips the allocation entirely.
|
|
90
|
+
let buf: Uint8Array;
|
|
91
|
+
if (this.#partial) {
|
|
92
|
+
buf = new Uint8Array(this.#partial.length + chunk.length);
|
|
93
|
+
buf.set(this.#partial, 0);
|
|
94
|
+
buf.set(chunk, this.#partial.length);
|
|
95
|
+
this.#partial = null;
|
|
96
|
+
} else {
|
|
97
|
+
buf = chunk;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const len = buf.length;
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < len) {
|
|
103
|
+
const lf = buf.indexOf(0x0a, i);
|
|
104
|
+
if (lf === -1) {
|
|
105
|
+
// Retain the tail as a partial line for the next chunk. Copy
|
|
106
|
+
// because the source `chunk` buffer may be reused upstream.
|
|
107
|
+
this.#partial = buf.subarray(i).slice();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let end = lf;
|
|
111
|
+
if (end > i && buf[end - 1] === 0x0d) end--;
|
|
112
|
+
this.#consumeLine(buf, i, end);
|
|
113
|
+
i = lf + 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
flush(): void {
|
|
118
|
+
// Treat any trailing partial line (no terminating LF) as a complete line.
|
|
119
|
+
if (this.#partial) {
|
|
120
|
+
const tail = this.#partial;
|
|
121
|
+
this.#partial = null;
|
|
122
|
+
let end = tail.length;
|
|
123
|
+
if (end > 0 && tail[end - 1] === 0x0d) end--;
|
|
124
|
+
if (end > 0) this.#consumeLine(tail, 0, end);
|
|
125
|
+
}
|
|
126
|
+
// Real services don't always close on a blank line — flush any pending event.
|
|
127
|
+
this.#dispatch();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#consumeLine(buf: Uint8Array, start: number, end: number): void {
|
|
131
|
+
if (end === start) {
|
|
132
|
+
this.#dispatch();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Comment line: keep verbatim in `raw` for diagnostic context, skip parsing.
|
|
136
|
+
// SSE spec § 9.2.6: lines beginning with ':' are heartbeats/comments and
|
|
137
|
+
// MUST NOT contribute to the event dispatch state. Heartbeats are the
|
|
138
|
+
// single most common line type on long-poll provider streams, so the
|
|
139
|
+
// early-return here directly avoids ~half the field-parse work.
|
|
140
|
+
if (buf[start] === 0x3a /* ':' */) {
|
|
141
|
+
this.#raw.push(decodeSseLine(buf, start, end));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Byte-level field parse. We avoid `text.indexOf(':')` + two `String.slice`
|
|
145
|
+
// calls (~6% of CPU pre-optimization) by scanning bytes for the field
|
|
146
|
+
// delimiter and matching the field name byte-for-byte. Field-name bytes
|
|
147
|
+
// are ASCII per SSE spec, so byte offsets equal char offsets in the
|
|
148
|
+
// decoded string and we can `slice` the value directly off `text` without
|
|
149
|
+
// re-decoding.
|
|
150
|
+
//
|
|
151
|
+
// ASCII signatures (verified against SSE spec):
|
|
152
|
+
// "event" = 0x65 0x76 0x65 0x6e 0x74 (5 bytes)
|
|
153
|
+
// "data" = 0x64 0x61 0x74 0x61 (4 bytes)
|
|
154
|
+
let colon = -1;
|
|
155
|
+
for (let k = start; k < end; k++) {
|
|
156
|
+
if (buf[k] === 0x3a) {
|
|
157
|
+
colon = k;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const fieldEnd = colon === -1 ? end : colon;
|
|
162
|
+
let valueStart = colon === -1 ? end : colon + 1;
|
|
163
|
+
// Per SSE spec, a single leading SP after the colon is stripped.
|
|
164
|
+
if (valueStart < end && buf[valueStart] === 0x20 /* ' ' */) valueStart++;
|
|
165
|
+
const fieldLen = fieldEnd - start;
|
|
166
|
+
const isEvent =
|
|
167
|
+
fieldLen === 5 &&
|
|
168
|
+
buf[start] === 0x65 &&
|
|
169
|
+
buf[start + 1] === 0x76 &&
|
|
170
|
+
buf[start + 2] === 0x65 &&
|
|
171
|
+
buf[start + 3] === 0x6e &&
|
|
172
|
+
buf[start + 4] === 0x74;
|
|
173
|
+
const isData =
|
|
174
|
+
!isEvent &&
|
|
175
|
+
fieldLen === 4 &&
|
|
176
|
+
buf[start] === 0x64 &&
|
|
177
|
+
buf[start + 1] === 0x61 &&
|
|
178
|
+
buf[start + 2] === 0x74 &&
|
|
179
|
+
buf[start + 3] === 0x61;
|
|
180
|
+
// Decode the line exactly once. Raw observers (debug buffer) want it
|
|
181
|
+
// regardless of field kind; `id`/`retry`/unknown lines pay only the
|
|
182
|
+
// decode cost, not any extra slicing.
|
|
183
|
+
const text = decodeSseLine(buf, start, end);
|
|
184
|
+
this.#raw.push(text);
|
|
185
|
+
if (isEvent) {
|
|
186
|
+
// `valueStart - start` is a byte offset into the line; since the
|
|
187
|
+
// "event:" prefix (and the optional SP) are pure ASCII, that byte
|
|
188
|
+
// offset equals the char offset in the decoded `text`.
|
|
189
|
+
this.#event = valueStart === end ? "" : text.slice(valueStart - start);
|
|
190
|
+
} else if (isData) {
|
|
191
|
+
const value = valueStart === end ? "" : text.slice(valueStart - start);
|
|
192
|
+
if (this.#data === null) this.#data = value;
|
|
193
|
+
else this.#data = `${this.#data}\n${value}`;
|
|
194
|
+
}
|
|
195
|
+
// `id` and `retry` are intentionally ignored — providers don't use them
|
|
196
|
+
// and reconnects are handled by the underlying transport.
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Hands ownership of the accumulated `raw` array to the observer. The
|
|
200
|
+
// observer (currently only `RawSseDebugBuffer.recordEvent`) MAY retain the
|
|
201
|
+
// array; we install a fresh `#raw = []` for the next event before invoking
|
|
202
|
+
// the observer so there is no aliasing across dispatches. This contract is
|
|
203
|
+
// mirrored in `notifyRawSseEvent` (no defensive clone) — see its comment.
|
|
204
|
+
//
|
|
205
|
+
// TODO(BufferOpt): once the buffer-side audit confirms it never mutates
|
|
206
|
+
// `event.raw`, the defensive `[...event.raw]` clone in older call paths
|
|
207
|
+
// (search for `notifyRawSseEvent`) can be dropped repository-wide.
|
|
208
|
+
#dispatch(): void {
|
|
209
|
+
if (this.#event === null && this.#data === null) return;
|
|
210
|
+
const event: RawSseEvent = {
|
|
211
|
+
event: this.#event,
|
|
212
|
+
data: this.#data ?? "",
|
|
213
|
+
raw: this.#raw,
|
|
214
|
+
};
|
|
215
|
+
this.#event = null;
|
|
216
|
+
this.#data = null;
|
|
217
|
+
this.#raw = [];
|
|
218
|
+
try {
|
|
219
|
+
this.#observer(event);
|
|
220
|
+
} catch {
|
|
221
|
+
// Raw stream observers are diagnostic only and must not affect generation.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function wrapFetchForSseDebug(
|
|
227
|
+
fetchImpl: FetchWithPreconnect,
|
|
228
|
+
observer: RawSseObserver | undefined,
|
|
229
|
+
): FetchWithPreconnect {
|
|
230
|
+
if (!observer) return fetchImpl;
|
|
231
|
+
|
|
232
|
+
const wrapped = Object.assign(
|
|
233
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
234
|
+
const response = await fetchImpl(input, init);
|
|
235
|
+
if (!isSseResponse(response)) {
|
|
236
|
+
return response;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const body = response.body;
|
|
240
|
+
if (!body) return response;
|
|
241
|
+
|
|
242
|
+
// Single-pass interception. Previously implemented as
|
|
243
|
+
// `body.pipeThrough(new TransformStream({...}))`, but the WHATWG
|
|
244
|
+
// TransformStream machinery imposes a per-chunk Promise boundary
|
|
245
|
+
// (`#handleNumberResult` showed at 8.8% self-time in CPU profile).
|
|
246
|
+
// A manual ReadableStream pulling directly from `body.getReader()`
|
|
247
|
+
// skips that hop: every `read()` immediately feeds both the parser
|
|
248
|
+
// and the controller in the same microtask.
|
|
249
|
+
const parser = new SseTeeParser(observer);
|
|
250
|
+
const reader = body.getReader();
|
|
251
|
+
const teed = new ReadableStream<Uint8Array>({
|
|
252
|
+
async pull(controller) {
|
|
253
|
+
try {
|
|
254
|
+
const { done, value } = await reader.read();
|
|
255
|
+
if (done) {
|
|
256
|
+
parser.flush();
|
|
257
|
+
controller.close();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Enqueue first so the consumer sees bytes ASAP; parser
|
|
261
|
+
// dispatch is best-effort diagnostic and runs after.
|
|
262
|
+
controller.enqueue(value);
|
|
263
|
+
parser.push(value);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Mirror TransformStream semantics: surface upstream
|
|
266
|
+
// errors to the consumer; do not flush a partial event.
|
|
267
|
+
controller.error(err);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
cancel(reason) {
|
|
271
|
+
// Propagate downstream cancellation to the source body so the
|
|
272
|
+
// underlying connection is released. Matches `pipeThrough`'s
|
|
273
|
+
// cancel-propagation behavior; `flush()` is intentionally NOT
|
|
274
|
+
// called (TransformStream skips `flush` on abort too).
|
|
275
|
+
return reader.cancel(reason);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return new Response(teed, {
|
|
280
|
+
status: response.status,
|
|
281
|
+
statusText: response.statusText,
|
|
282
|
+
headers: response.headers,
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
fetchImpl.preconnect ? { preconnect: fetchImpl.preconnect } : {},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return wrapped;
|
|
289
|
+
}
|