@prometheus-ai/ai 0.5.4 → 0.5.8
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/dist/types/auth-broker/remote-store.d.ts +2 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +4 -1
- package/dist/types/auth-gateway/server.d.ts +19 -0
- package/dist/types/auth-gateway/types.d.ts +9 -3
- package/dist/types/auth-retry.d.ts +119 -0
- package/dist/types/auth-storage.d.ts +217 -8
- package/dist/types/errors.d.ts +24 -0
- package/dist/types/index.d.ts +5 -9
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/providers/amazon-bedrock.d.ts +12 -6
- package/dist/types/providers/anthropic-client.d.ts +10 -3
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +2 -2
- package/dist/types/providers/anthropic-messages-server.d.ts +3 -3
- package/dist/types/providers/anthropic-wire.d.ts +3 -3
- package/dist/types/providers/anthropic.d.ts +41 -34
- package/dist/types/providers/aws-credentials.d.ts +8 -0
- package/dist/types/providers/azure-openai-responses.d.ts +1 -0
- package/dist/types/providers/google-gemini-cli.d.ts +22 -1
- package/dist/types/providers/google-shared.d.ts +22 -0
- package/dist/types/providers/google-types.d.ts +13 -1
- package/dist/types/providers/mock.d.ts +8 -3
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +6 -3
- package/dist/types/providers/openai-chat-server.d.ts +3 -3
- package/dist/types/providers/openai-chat-wire.d.ts +644 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +8 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +9 -0
- package/dist/types/providers/openai-codex-responses.d.ts +31 -2
- package/dist/types/providers/openai-completions-compat.d.ts +2 -25
- package/dist/types/providers/openai-completions.d.ts +2 -10
- package/dist/types/providers/openai-responses-server-schema.d.ts +4 -4
- package/dist/types/providers/openai-responses-server.d.ts +2 -2
- package/dist/types/providers/openai-responses-shared.d.ts +49 -9
- package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
- package/dist/types/providers/openai-responses.d.ts +13 -4
- package/dist/types/providers/prometheus-native-client.d.ts +9 -0
- package/dist/types/providers/prometheus-native-server.d.ts +4 -3
- package/dist/types/providers/transform-messages.d.ts +1 -2
- package/dist/types/rate-limit-utils.d.ts +3 -2
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +7 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/{utils/oauth → registry}/api-key-login.d.ts +8 -2
- package/dist/types/{utils/oauth → registry}/api-key-validation.d.ts +15 -0
- package/dist/types/registry/cerebras.d.ts +7 -0
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
- package/dist/types/registry/cursor.d.ts +7 -0
- package/dist/types/registry/deepseek.d.ts +8 -0
- package/dist/types/registry/derived.d.ts +5 -0
- package/dist/types/registry/firepass.d.ts +16 -0
- package/dist/types/registry/fireworks.d.ts +7 -0
- package/dist/types/registry/github-copilot.d.ts +7 -0
- package/dist/types/registry/gitlab-duo.d.ts +9 -0
- package/dist/types/registry/google-antigravity.d.ts +9 -0
- package/dist/types/registry/google-gemini-cli.d.ts +9 -0
- package/dist/types/registry/google-vertex.d.ts +5 -0
- package/dist/types/registry/google.d.ts +4 -0
- package/dist/types/registry/groq.d.ts +4 -0
- package/dist/types/registry/huggingface.d.ts +7 -0
- package/dist/types/registry/index.d.ts +4 -0
- package/dist/types/registry/kagi.d.ts +14 -0
- package/dist/types/registry/kilo.d.ts +7 -0
- package/dist/types/registry/kimi-code.d.ts +7 -0
- package/dist/types/registry/litellm.d.ts +13 -0
- package/dist/types/registry/lm-studio.d.ts +8 -0
- package/dist/types/registry/minimax-code-cn.d.ts +6 -0
- package/dist/types/registry/minimax-code.d.ts +6 -0
- package/dist/types/registry/minimax.d.ts +4 -0
- package/dist/types/registry/mistral.d.ts +4 -0
- package/dist/types/registry/moonshot.d.ts +7 -0
- package/dist/types/registry/nanogpt.d.ts +7 -0
- package/dist/types/registry/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
- package/dist/types/{utils → registry}/oauth/anthropic.d.ts +2 -1
- package/dist/types/{utils → registry}/oauth/github-copilot.d.ts +15 -23
- package/dist/types/{utils → registry}/oauth/index.d.ts +1 -0
- package/dist/types/{utils → registry}/oauth/minimax-code.d.ts +5 -5
- package/dist/types/{utils → registry}/oauth/types.d.ts +6 -1
- package/dist/types/{utils → registry}/oauth/xai-oauth.d.ts +2 -1
- package/dist/types/registry/ollama-cloud.d.ts +7 -0
- package/dist/types/registry/ollama.d.ts +12 -0
- package/dist/types/registry/openai-codex-device.d.ts +8 -0
- package/dist/types/registry/openai-codex.d.ts +9 -0
- package/dist/types/registry/openai.d.ts +4 -0
- package/dist/types/registry/opencode-go.d.ts +6 -0
- package/dist/types/registry/opencode-zen.d.ts +6 -0
- package/dist/types/registry/openrouter.d.ts +13 -0
- package/dist/types/registry/parallel.d.ts +14 -0
- package/dist/types/registry/perplexity.d.ts +7 -0
- package/dist/types/registry/qianfan.d.ts +7 -0
- package/dist/types/registry/qwen-portal.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +272 -0
- package/dist/types/registry/synthetic.d.ts +6 -0
- package/dist/types/registry/tavily.d.ts +14 -0
- package/dist/types/registry/together.d.ts +6 -0
- package/dist/types/registry/types.d.ts +51 -0
- package/dist/types/registry/venice.d.ts +13 -0
- package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
- package/dist/types/registry/vllm.d.ts +7 -0
- package/dist/types/registry/wafer-pass.d.ts +6 -0
- package/dist/types/registry/wafer-serverless.d.ts +6 -0
- package/dist/types/registry/xai-oauth.d.ts +7 -0
- package/dist/types/registry/xai.d.ts +4 -0
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
- package/dist/types/registry/xiaomi.d.ts +6 -0
- package/dist/types/registry/zai.d.ts +7 -0
- package/dist/types/registry/zenmux.d.ts +7 -0
- package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
- package/dist/types/stream.d.ts +9 -1
- package/dist/types/types.d.ts +56 -295
- package/dist/types/usage/google-antigravity.d.ts +15 -1
- package/dist/types/usage/openai-codex-reset.d.ts +79 -0
- package/dist/types/usage/openai-codex.d.ts +1 -0
- package/dist/types/usage.d.ts +77 -4
- package/dist/types/utils/abort.d.ts +6 -0
- package/dist/types/utils/event-stream.d.ts +2 -0
- package/dist/types/utils/http-inspector.d.ts +0 -1
- package/dist/types/utils/idle-iterator.d.ts +35 -0
- package/dist/types/utils/openai-http.d.ts +58 -0
- package/dist/types/utils/request-debug.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +1 -0
- package/dist/types/utils/schema/fields.d.ts +5 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +7 -15
- package/dist/types/utils/sse-debug.d.ts +0 -5
- package/dist/types/utils/stream-markup-healing.d.ts +2 -0
- package/dist/types/utils.d.ts +1 -5
- package/package.json +17 -29
- package/src/auth-broker/remote-store.ts +10 -1
- package/src/auth-broker/snapshot-cache.ts +1 -1
- package/src/auth-broker/wire-schemas.ts +1 -1
- package/src/auth-gateway/http.ts +1 -1
- package/src/auth-gateway/server.ts +95 -30
- package/src/auth-gateway/types.ts +10 -2
- package/src/auth-retry.ts +238 -0
- package/src/auth-storage.ts +935 -430
- package/src/errors.ts +32 -0
- package/src/index.ts +9 -14
- package/src/provider-details.ts +1 -1
- package/src/providers/__tests__/google-auth.test.ts +144 -0
- package/src/providers/amazon-bedrock.ts +70 -40
- package/src/providers/anthropic-client.ts +15 -13
- package/src/providers/anthropic-messages-server-schema.ts +17 -7
- package/src/providers/anthropic-messages-server.ts +88 -20
- package/src/providers/anthropic-wire.ts +4 -3
- package/src/providers/anthropic.ts +1234 -621
- package/src/providers/aws-credentials.ts +47 -5
- package/src/providers/aws-eventstream.ts +5 -0
- package/src/providers/azure-openai-responses.ts +117 -67
- package/src/providers/cursor.ts +30 -30
- package/src/providers/github-copilot-headers.ts +1 -1
- package/src/providers/gitlab-duo.ts +36 -29
- package/src/providers/google-auth.ts +71 -8
- package/src/providers/google-gemini-cli.ts +118 -22
- package/src/providers/google-shared.ts +163 -43
- package/src/providers/google-types.ts +10 -1
- package/src/providers/kimi.ts +1 -1
- package/src/providers/mock.ts +11 -3
- package/src/providers/ollama.ts +64 -7
- package/src/providers/openai-anthropic-shim.ts +17 -8
- package/src/providers/openai-chat-server-schema.ts +9 -3
- package/src/providers/openai-chat-server.ts +82 -16
- package/src/providers/openai-chat-wire.ts +847 -0
- package/src/providers/openai-codex/request-transformer.ts +129 -34
- package/src/providers/openai-codex/response-handler.ts +22 -1
- package/src/providers/openai-codex-responses.ts +699 -247
- package/src/providers/openai-completions-compat.ts +8 -308
- package/src/providers/openai-completions.ts +416 -267
- package/src/providers/openai-responses-server-schema.ts +15 -9
- package/src/providers/openai-responses-server.ts +162 -114
- package/src/providers/openai-responses-shared.ts +320 -82
- package/src/providers/openai-responses-wire.ts +6391 -0
- package/src/providers/openai-responses.ts +382 -176
- package/src/providers/prometheus-native-client.ts +27 -11
- package/src/providers/prometheus-native-server.ts +44 -17
- package/src/providers/transform-messages.ts +311 -120
- package/src/providers/vision-guard.ts +5 -3
- package/src/rate-limit-utils.ts +13 -3
- package/src/registry/aimlapi.ts +6 -0
- package/src/{utils/oauth → registry}/alibaba-coding-plan.ts +8 -18
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/{utils/oauth → registry}/api-key-login.ts +25 -3
- package/src/{utils/oauth → registry}/api-key-validation.ts +62 -2
- package/src/{utils/oauth → registry}/cerebras.ts +8 -1
- package/src/{utils/oauth → registry}/cloudflare-ai-gateway.ts +8 -12
- package/src/registry/cursor.ts +20 -0
- package/src/{utils/oauth → registry}/deepseek.ts +9 -17
- package/src/registry/derived.ts +9 -0
- package/src/{utils/oauth → registry}/firepass.ts +10 -2
- package/src/{utils/oauth → registry}/fireworks.ts +8 -1
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/google-antigravity.ts +21 -0
- package/src/registry/google-gemini-cli.ts +21 -0
- package/src/registry/google-vertex.ts +38 -0
- package/src/registry/google.ts +6 -0
- package/src/registry/groq.ts +6 -0
- package/src/{utils/oauth → registry}/huggingface.ts +8 -19
- package/src/registry/index.ts +4 -0
- package/src/{utils/oauth → registry}/kagi.ts +9 -11
- package/src/{utils/oauth → registry}/kilo.ts +11 -6
- package/src/registry/kimi-code.ts +17 -0
- package/src/{utils/oauth → registry}/litellm.ts +8 -12
- package/src/{utils/oauth → registry}/lm-studio.ts +9 -17
- package/src/registry/minimax-code-cn.ts +12 -0
- package/src/registry/minimax-code.ts +12 -0
- package/src/registry/minimax.ts +6 -0
- package/src/registry/mistral.ts +6 -0
- package/src/{utils/oauth → registry}/moonshot.ts +8 -9
- package/src/{utils/oauth → registry}/nanogpt.ts +8 -1
- package/src/{utils/oauth → registry}/nvidia.ts +8 -18
- package/src/{utils → registry}/oauth/__tests__/xai-oauth.test.ts +4 -7
- package/src/{utils → registry}/oauth/anthropic.ts +38 -17
- package/src/{utils → registry}/oauth/github-copilot.ts +79 -115
- package/src/registry/oauth/gitlab-duo.ts +198 -0
- package/src/{utils → registry}/oauth/google-antigravity.ts +1 -4
- package/src/{utils → registry}/oauth/google-gemini-cli.ts +1 -4
- package/src/registry/oauth/index.ts +164 -0
- package/src/{utils → registry}/oauth/minimax-code.ts +16 -14
- package/src/{utils → registry}/oauth/types.ts +7 -51
- package/src/{utils → registry}/oauth/wafer.ts +1 -1
- package/src/{utils → registry}/oauth/xai-oauth.ts +16 -8
- package/src/{utils → registry}/oauth/xiaomi.ts +9 -4
- package/src/{utils/oauth → registry}/ollama-cloud.ts +8 -1
- package/src/{utils/oauth → registry}/ollama.ts +8 -13
- package/src/registry/openai-codex-device.ts +18 -0
- package/src/registry/openai-codex.ts +19 -0
- package/src/registry/openai.ts +6 -0
- package/src/registry/opencode-go.ts +12 -0
- package/src/registry/opencode-zen.ts +12 -0
- package/src/{utils/oauth → registry}/openrouter.ts +10 -2
- package/src/{utils/oauth → registry}/parallel.ts +9 -11
- package/src/registry/perplexity.ts +13 -0
- package/src/{utils/oauth → registry}/qianfan.ts +8 -17
- package/src/{utils/oauth → registry}/qwen-portal.ts +8 -19
- package/src/registry/registry.ts +149 -0
- package/src/{utils/oauth → registry}/synthetic.ts +7 -1
- package/src/{utils/oauth → registry}/tavily.ts +10 -12
- package/src/{utils/oauth → registry}/together.ts +7 -1
- package/src/registry/types.ts +56 -0
- package/src/{utils/oauth → registry}/venice.ts +8 -12
- package/src/{utils/oauth → registry}/vercel-ai-gateway.ts +8 -18
- package/src/{utils/oauth → registry}/vllm.ts +9 -16
- package/src/registry/wafer-pass.ts +12 -0
- package/src/registry/wafer-serverless.ts +12 -0
- package/src/registry/xai-oauth.ts +17 -0
- package/src/registry/xai.ts +6 -0
- package/src/registry/xiaomi-token-plan-ams.ts +12 -0
- package/src/registry/xiaomi-token-plan-cn.ts +12 -0
- package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
- package/src/registry/xiaomi.ts +12 -0
- package/src/{utils/oauth → registry}/zai.ts +10 -22
- package/src/{utils/oauth → registry}/zenmux.ts +8 -1
- package/src/{utils/oauth/zhipu.ts → registry/zhipu-coding-plan.ts} +9 -21
- package/src/stream.ts +229 -199
- package/src/types.ts +63 -384
- package/src/usage/claude.ts +4 -2
- package/src/usage/github-copilot.ts +4 -2
- package/src/usage/google-antigravity.ts +196 -28
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +5 -6
- package/src/usage/openai-codex-reset.ts +174 -0
- package/src/usage/openai-codex.ts +19 -2
- package/src/usage/zai.ts +2 -1
- package/src/usage.ts +93 -4
- package/src/utils/abort.ts +14 -0
- package/src/utils/event-stream.ts +17 -0
- package/src/utils/http-inspector.ts +4 -12
- package/src/utils/idle-iterator.ts +250 -79
- package/src/utils/openai-http.ts +157 -0
- package/src/utils/request-debug.ts +67 -19
- package/src/utils/retry-after.ts +1 -1
- package/src/utils/retry.ts +23 -2
- package/src/utils/schema/CONSTRAINTS.md +4 -2
- package/src/utils/schema/fields.ts +16 -0
- package/src/utils/schema/json-schema-validator.ts +19 -1
- package/src/utils/schema/normalize.ts +80 -8
- package/src/utils/schema/stamps.ts +22 -10
- package/src/utils/schema/wire.ts +2 -2
- package/src/utils/sse-debug.ts +0 -271
- package/src/utils/stream-markup-healing.ts +50 -8
- package/src/utils/validation.ts +49 -13
- package/src/utils.ts +2 -26
- package/dist/types/model-cache.d.ts +0 -17
- package/dist/types/model-manager.d.ts +0 -64
- package/dist/types/model-thinking.d.ts +0 -100
- package/dist/types/models.d.ts +0 -12
- package/dist/types/provider-models/bundled-references.d.ts +0 -4
- package/dist/types/provider-models/descriptors.d.ts +0 -50
- package/dist/types/provider-models/google.d.ts +0 -24
- package/dist/types/provider-models/index.d.ts +0 -5
- package/dist/types/provider-models/ollama.d.ts +0 -7
- package/dist/types/provider-models/openai-compat.d.ts +0 -323
- package/dist/types/provider-models/special.d.ts +0 -16
- package/dist/types/utils/discovery/antigravity.d.ts +0 -61
- package/dist/types/utils/discovery/codex.d.ts +0 -38
- package/dist/types/utils/discovery/cursor.d.ts +0 -23
- package/dist/types/utils/discovery/gemini.d.ts +0 -25
- package/dist/types/utils/discovery/index.d.ts +0 -4
- package/dist/types/utils/discovery/openai-compatible.d.ts +0 -72
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
- package/dist/types/utils/oauth/cerebras.d.ts +0 -1
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/deepseek.d.ts +0 -10
- package/dist/types/utils/oauth/firepass.d.ts +0 -1
- package/dist/types/utils/oauth/fireworks.d.ts +0 -1
- package/dist/types/utils/oauth/huggingface.d.ts +0 -19
- package/dist/types/utils/oauth/kagi.d.ts +0 -17
- package/dist/types/utils/oauth/kilo.d.ts +0 -5
- package/dist/types/utils/oauth/litellm.d.ts +0 -18
- package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
- package/dist/types/utils/oauth/moonshot.d.ts +0 -1
- package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
- package/dist/types/utils/oauth/nvidia.d.ts +0 -18
- package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
- package/dist/types/utils/oauth/ollama.d.ts +0 -18
- package/dist/types/utils/oauth/openrouter.d.ts +0 -1
- package/dist/types/utils/oauth/parallel.d.ts +0 -17
- package/dist/types/utils/oauth/qianfan.d.ts +0 -17
- package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
- package/dist/types/utils/oauth/synthetic.d.ts +0 -1
- package/dist/types/utils/oauth/tavily.d.ts +0 -17
- package/dist/types/utils/oauth/together.d.ts +0 -1
- package/dist/types/utils/oauth/venice.d.ts +0 -18
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/vllm.d.ts +0 -16
- package/dist/types/utils/oauth/zai.d.ts +0 -18
- package/dist/types/utils/oauth/zenmux.d.ts +0 -1
- package/dist/types/utils/oauth/zhipu.d.ts +0 -18
- package/src/model-cache.ts +0 -129
- package/src/model-manager.ts +0 -469
- package/src/model-thinking.ts +0 -756
- package/src/models.json +0 -60287
- package/src/models.json.d.ts +0 -9
- package/src/models.ts +0 -56
- package/src/provider-models/bundled-references.ts +0 -38
- package/src/provider-models/descriptors.ts +0 -364
- package/src/provider-models/google.ts +0 -88
- package/src/provider-models/index.ts +0 -5
- package/src/provider-models/ollama.ts +0 -153
- package/src/provider-models/openai-compat.ts +0 -2904
- package/src/provider-models/special.ts +0 -67
- package/src/utils/discovery/antigravity.ts +0 -261
- package/src/utils/discovery/codex.ts +0 -371
- package/src/utils/discovery/cursor.ts +0 -306
- package/src/utils/discovery/gemini.ts +0 -248
- package/src/utils/discovery/index.ts +0 -4
- package/src/utils/discovery/openai-compatible.ts +0 -224
- package/src/utils/oauth/gitlab-duo.ts +0 -123
- package/src/utils/oauth/index.ts +0 -502
- /package/dist/types/{utils/oauth/__tests__/xai-oauth.test.d.ts → providers/__tests__/google-auth.test.d.ts} +0 -0
- /package/dist/types/{utils → registry}/oauth/callback-server.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/cursor.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/gitlab-duo.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-antigravity.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-gemini-cli.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-oauth-shared.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/kimi.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/openai-codex.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/opencode.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/perplexity.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/pkce.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/wafer.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/xiaomi.d.ts +0 -0
- /package/src/{utils → registry}/oauth/callback-server.ts +0 -0
- /package/src/{utils → registry}/oauth/cursor.ts +0 -0
- /package/src/{utils → registry}/oauth/google-oauth-shared.ts +0 -0
- /package/src/{utils → registry}/oauth/kimi.ts +0 -0
- /package/src/{utils → registry}/oauth/oauth.html +0 -0
- /package/src/{utils → registry}/oauth/openai-codex.ts +0 -0
- /package/src/{utils → registry}/oauth/opencode.ts +0 -0
- /package/src/{utils → registry}/oauth/perplexity.ts +0 -0
- /package/src/{utils → registry}/oauth/pkce.ts +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-POST → SSE transport for OpenAI-wire streaming endpoints (chat
|
|
3
|
+
* completions, responses, azure responses). Replaces the `openai` SDK client:
|
|
4
|
+
*
|
|
5
|
+
* - Retries: `fetchWithRetry` (Retry-After/quota-hint aware; 5xx/408/429 and
|
|
6
|
+
* transient network errors). Default 6 total attempts — parity with the
|
|
7
|
+
* SDK's former `maxRetries: 5`.
|
|
8
|
+
* - SSE decode: `readSseJson` (spec-compliant framing, `[DONE]`-aware).
|
|
9
|
+
* `onSseEvent` observers now receive real wire frames instead of events
|
|
10
|
+
* re-synthesized from decoded SDK objects.
|
|
11
|
+
* - Errors: {@link OpenAIHttpError} exposes `status`/`headers`/`code`
|
|
12
|
+
* structurally (ProviderHttpError contract — `extractHttpStatusFromError`,
|
|
13
|
+
* retry-after extraction, copilot transient classification) and carries the
|
|
14
|
+
* captured response body for the strict-tools fallback and the responses
|
|
15
|
+
* chain-state detectors, which regex over `error.message`.
|
|
16
|
+
*/
|
|
17
|
+
import { fetchWithRetry, readSseJson, type SseEventObserver } from "@prometheus-ai/utils";
|
|
18
|
+
import { ProviderHttpError } from "../errors";
|
|
19
|
+
import type { FetchImpl } from "../types";
|
|
20
|
+
import type { CapturedHttpErrorResponse } from "./http-inspector";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Total attempts when the caller has no first-event deadline armed. The
|
|
24
|
+
* removed SDK clients ran `maxRetries: 5`, i.e. 6 requests.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_MAX_ATTEMPTS = 6;
|
|
27
|
+
|
|
28
|
+
/** Bound the `Error.message` allocation for proxy HTML error pages and the like. */
|
|
29
|
+
const MAX_DETAIL_CHARS = 4096;
|
|
30
|
+
|
|
31
|
+
/** Non-2xx response from an OpenAI-wire endpoint, with the decoded body attached. */
|
|
32
|
+
export class OpenAIHttpError extends ProviderHttpError {
|
|
33
|
+
readonly captured: CapturedHttpErrorResponse;
|
|
34
|
+
|
|
35
|
+
constructor(message: string, captured: CapturedHttpErrorResponse, code: string | undefined) {
|
|
36
|
+
super(message, captured.status, { headers: captured.headers, code });
|
|
37
|
+
this.name = "OpenAIHttpError";
|
|
38
|
+
this.captured = captured;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OpenAIStreamRequestInit {
|
|
43
|
+
url: string;
|
|
44
|
+
headers: Record<string, string>;
|
|
45
|
+
/** JSON request body; serialized once per call (retries resend the same bytes). */
|
|
46
|
+
body: unknown;
|
|
47
|
+
signal: AbortSignal;
|
|
48
|
+
fetch?: FetchImpl;
|
|
49
|
+
/**
|
|
50
|
+
* Total attempts (initial + retries). Defaults to {@link DEFAULT_MAX_ATTEMPTS}.
|
|
51
|
+
* Pass `1` when a first-event watchdog is armed so retries cannot silently
|
|
52
|
+
* extend the caller's deadline (mirrors the old `maxRetries: 0` hint).
|
|
53
|
+
*/
|
|
54
|
+
maxAttempts?: number;
|
|
55
|
+
/** Raw wire-frame observer (`onSseEvent` debug pipeline). */
|
|
56
|
+
onSseEvent?: SseEventObserver;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface OpenAIStreamHandle<TEvent> {
|
|
60
|
+
/** Decoded `data:` payloads; terminates on `[DONE]` or stream end. */
|
|
61
|
+
events: AsyncGenerator<TEvent>;
|
|
62
|
+
response: Response;
|
|
63
|
+
/** `x-request-id` response header (the SDK's former `request_id`). */
|
|
64
|
+
requestId: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* POST a JSON body and stream back decoded SSE events.
|
|
69
|
+
*
|
|
70
|
+
* Throws {@link OpenAIHttpError} on a non-2xx terminal response. Aborts on
|
|
71
|
+
* `signal` propagate from `fetchWithRetry`/`readSseJson`; callers own the
|
|
72
|
+
* watchdog timers and abort-reason bookkeeping.
|
|
73
|
+
*/
|
|
74
|
+
export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): Promise<OpenAIStreamHandle<TEvent>> {
|
|
75
|
+
const response = await fetchWithRetry(init.url, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...init.headers },
|
|
78
|
+
body: JSON.stringify(init.body),
|
|
79
|
+
signal: init.signal,
|
|
80
|
+
fetch: init.fetch,
|
|
81
|
+
maxAttempts: init.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
82
|
+
// Bun's native fetch enforces a hard ~300s pre-response timeout (issue #2422).
|
|
83
|
+
// Cold large-context streams legitimately exceed it; the caller's
|
|
84
|
+
// `firstEventTimeoutMs`/`AbortSignal` already govern stuck requests.
|
|
85
|
+
timeout: false,
|
|
86
|
+
});
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw await captureOpenAIHttpError(response);
|
|
89
|
+
}
|
|
90
|
+
if (!response.body) {
|
|
91
|
+
throw new Error(`OpenAI stream response has no body (status ${response.status})`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
events: readSseJson<TEvent>(response.body, init.signal, init.onSseEvent),
|
|
95
|
+
response,
|
|
96
|
+
requestId: response.headers.get("x-request-id"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Decode a non-2xx response into an {@link OpenAIHttpError} without consuming it twice. */
|
|
101
|
+
export async function captureOpenAIHttpError(response: Response): Promise<OpenAIHttpError> {
|
|
102
|
+
let bodyText: string | undefined;
|
|
103
|
+
let bodyJson: unknown;
|
|
104
|
+
try {
|
|
105
|
+
bodyText = await response.text();
|
|
106
|
+
if (bodyText.trim().length > 0) {
|
|
107
|
+
try {
|
|
108
|
+
bodyJson = JSON.parse(bodyText);
|
|
109
|
+
} catch {}
|
|
110
|
+
} else {
|
|
111
|
+
bodyText = undefined;
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
const captured: CapturedHttpErrorResponse = {
|
|
115
|
+
status: response.status,
|
|
116
|
+
headers: response.headers,
|
|
117
|
+
bodyText,
|
|
118
|
+
bodyJson,
|
|
119
|
+
};
|
|
120
|
+
const { detail, code } = extractErrorDetail(bodyJson, bodyText);
|
|
121
|
+
// "status code (no body)" matches the SDK's former APIError phrasing;
|
|
122
|
+
// `finalizeErrorMessage` keys a repair path on that exact wording.
|
|
123
|
+
const message = detail
|
|
124
|
+
? `${response.status} ${detail.length > MAX_DETAIL_CHARS ? detail.slice(0, MAX_DETAIL_CHARS) : detail}`
|
|
125
|
+
: `${response.status} status code (no body)`;
|
|
126
|
+
return new OpenAIHttpError(message, captured, code);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Pull a human-readable message and machine code out of an OpenAI-style error
|
|
131
|
+
* envelope (`{ error: { message, code, type } }`), tolerating the flat shapes
|
|
132
|
+
* compat hosts return (`{ error: "..." }`, `{ message: "..." }`) and falling
|
|
133
|
+
* back to the raw body text.
|
|
134
|
+
*/
|
|
135
|
+
function extractErrorDetail(
|
|
136
|
+
bodyJson: unknown,
|
|
137
|
+
bodyText: string | undefined,
|
|
138
|
+
): { detail: string | undefined; code: string | undefined } {
|
|
139
|
+
if (typeof bodyJson === "object" && bodyJson !== null) {
|
|
140
|
+
const envelope = bodyJson as { error?: unknown; message?: unknown };
|
|
141
|
+
const error = envelope.error;
|
|
142
|
+
if (typeof error === "object" && error !== null) {
|
|
143
|
+
const { message, code, type } = error as { message?: unknown; code?: unknown; type?: unknown };
|
|
144
|
+
return {
|
|
145
|
+
detail: typeof message === "string" && message.length > 0 ? message : bodyText,
|
|
146
|
+
code: typeof code === "string" ? code : typeof type === "string" ? type : undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (typeof error === "string" && error.length > 0) {
|
|
150
|
+
return { detail: error, code: undefined };
|
|
151
|
+
}
|
|
152
|
+
if (typeof envelope.message === "string" && envelope.message.length > 0) {
|
|
153
|
+
return { detail: envelope.message, code: undefined };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { detail: bodyText, code: undefined };
|
|
157
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import type { FetchImpl } from "../types";
|
|
4
5
|
|
|
5
6
|
const REQUEST_DEBUG_ENV = "PROMETHEUS_REQ_DEBUG";
|
|
@@ -8,6 +9,7 @@ const textEncoder = new TextEncoder();
|
|
|
8
9
|
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
9
10
|
|
|
10
11
|
let nextSessionId = 1;
|
|
12
|
+
let nextRequestDebugPath: string | undefined;
|
|
11
13
|
|
|
12
14
|
type DebugFetch = FetchImpl & { [DEBUG_FETCH_MARKER]?: true };
|
|
13
15
|
type RequestBodyInit = NonNullable<RequestInit["body"]>;
|
|
@@ -27,6 +29,14 @@ export interface RequestDebugPayload {
|
|
|
27
29
|
protocol?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
interface ReservedRequestDebugFile {
|
|
33
|
+
id: number;
|
|
34
|
+
requestPath: string;
|
|
35
|
+
responsePath: string;
|
|
36
|
+
handle: fs.FileHandle;
|
|
37
|
+
overwrite: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
export interface RequestDebugResponseLog {
|
|
31
41
|
write(chunk: Uint8Array | string): void;
|
|
32
42
|
close(): Promise<void>;
|
|
@@ -40,10 +50,32 @@ export interface RequestDebugSession {
|
|
|
40
50
|
wrapResponse(response: Response): Promise<Response>;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
function isRequestDebugEnvEnabled(): boolean {
|
|
44
54
|
return Bun.env[REQUEST_DEBUG_ENV] === "1";
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
export function isRequestDebugEnabled(): boolean {
|
|
58
|
+
return isRequestDebugEnvEnabled() || nextRequestDebugPath !== undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setNextRequestDebugPath(requestPath: string): void {
|
|
62
|
+
nextRequestDebugPath = requestPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function clearNextRequestDebugPath(): void {
|
|
66
|
+
nextRequestDebugPath = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getNextRequestDebugPath(): string | undefined {
|
|
70
|
+
return nextRequestDebugPath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function consumeNextRequestDebugPath(): string | undefined {
|
|
74
|
+
const requestPath = nextRequestDebugPath;
|
|
75
|
+
nextRequestDebugPath = undefined;
|
|
76
|
+
return requestPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
export function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl {
|
|
48
80
|
if (!isRequestDebugEnabled()) return fetchImpl;
|
|
49
81
|
const maybeWrapped = fetchImpl as DebugFetch;
|
|
@@ -51,6 +83,7 @@ export function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl {
|
|
|
51
83
|
|
|
52
84
|
const wrapped = Object.assign(
|
|
53
85
|
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
86
|
+
if (!isRequestDebugEnabled()) return fetchImpl(input, init);
|
|
54
87
|
const session = await createFetchRequestDebugSession(input, init);
|
|
55
88
|
const response = await fetchImpl(input, init);
|
|
56
89
|
return session.wrapResponse(response);
|
|
@@ -69,7 +102,7 @@ export function withRequestDebugFetch<T extends { fetch?: FetchImpl } | undefine
|
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
export async function createRequestDebugSession(payload: RequestDebugPayload): Promise<RequestDebugSession> {
|
|
72
|
-
const { id, requestPath, responsePath, handle } = await reserveRequestDebugFile();
|
|
105
|
+
const { id, requestPath, responsePath, handle, overwrite } = await reserveRequestDebugFile();
|
|
73
106
|
const requestDump: Record<string, unknown> = {
|
|
74
107
|
id,
|
|
75
108
|
protocol: payload.protocol ?? "http",
|
|
@@ -89,7 +122,7 @@ export async function createRequestDebugSession(payload: RequestDebugPayload): P
|
|
|
89
122
|
await handle.close();
|
|
90
123
|
}
|
|
91
124
|
|
|
92
|
-
return new FileRequestDebugSession(id, requestPath, responsePath);
|
|
125
|
+
return new FileRequestDebugSession(id, requestPath, responsePath, overwrite);
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
async function createFetchRequestDebugSession(
|
|
@@ -110,15 +143,17 @@ class FileRequestDebugSession implements RequestDebugSession {
|
|
|
110
143
|
readonly id: number;
|
|
111
144
|
readonly requestPath: string;
|
|
112
145
|
readonly responsePath: string;
|
|
146
|
+
readonly #overwriteResponseLog: boolean;
|
|
113
147
|
|
|
114
|
-
constructor(id: number, requestPath: string, responsePath: string) {
|
|
148
|
+
constructor(id: number, requestPath: string, responsePath: string, overwriteResponseLog: boolean) {
|
|
115
149
|
this.id = id;
|
|
116
150
|
this.requestPath = requestPath;
|
|
117
151
|
this.responsePath = responsePath;
|
|
152
|
+
this.#overwriteResponseLog = overwriteResponseLog;
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
async openResponseLog(statusLine: string, headers?: RequestDebugHeaders): Promise<RequestDebugResponseLog> {
|
|
121
|
-
const handle = await fs.open(this.responsePath, "wx");
|
|
156
|
+
const handle = await fs.open(this.responsePath, this.#overwriteResponseLog ? "w" : "wx");
|
|
122
157
|
const headerBlock = formatResponseHeaderBlock(statusLine, headers);
|
|
123
158
|
await handle.write(textEncoder.encode(headerBlock));
|
|
124
159
|
return new FileRequestDebugResponseLog(handle);
|
|
@@ -170,6 +205,7 @@ class FileRequestDebugSession implements RequestDebugSession {
|
|
|
170
205
|
class FileRequestDebugResponseLog implements RequestDebugResponseLog {
|
|
171
206
|
#handle: fs.FileHandle | undefined;
|
|
172
207
|
#pending: Promise<void> = Promise.resolve();
|
|
208
|
+
#closed: Promise<void> | undefined;
|
|
173
209
|
|
|
174
210
|
constructor(handle: fs.FileHandle) {
|
|
175
211
|
this.#handle = handle;
|
|
@@ -184,15 +220,19 @@ class FileRequestDebugResponseLog implements RequestDebugResponseLog {
|
|
|
184
220
|
});
|
|
185
221
|
}
|
|
186
222
|
|
|
187
|
-
|
|
223
|
+
close(): Promise<void> {
|
|
224
|
+
if (this.#closed) return this.#closed;
|
|
188
225
|
const handle = this.#handle;
|
|
189
|
-
if (!handle) return;
|
|
226
|
+
if (!handle) return Promise.resolve();
|
|
190
227
|
this.#handle = undefined;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
this.#closed = (async () => {
|
|
229
|
+
try {
|
|
230
|
+
await this.#pending;
|
|
231
|
+
} finally {
|
|
232
|
+
await handle.close();
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
return this.#closed;
|
|
196
236
|
}
|
|
197
237
|
}
|
|
198
238
|
|
|
@@ -208,18 +248,26 @@ function copyResponseMetadata(target: Response, source: Response): void {
|
|
|
208
248
|
}
|
|
209
249
|
}
|
|
210
250
|
|
|
211
|
-
async function reserveRequestDebugFile(): Promise<{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
251
|
+
async function reserveRequestDebugFile(): Promise<ReservedRequestDebugFile> {
|
|
252
|
+
const explicitPath = consumeNextRequestDebugPath();
|
|
253
|
+
if (explicitPath) {
|
|
254
|
+
await fs.mkdir(path.dirname(explicitPath), { recursive: true });
|
|
255
|
+
const handle = await fs.open(explicitPath, "w");
|
|
256
|
+
return {
|
|
257
|
+
id: nextSessionId++,
|
|
258
|
+
requestPath: explicitPath,
|
|
259
|
+
responsePath: `${explicitPath}.res.log`,
|
|
260
|
+
handle,
|
|
261
|
+
overwrite: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
217
265
|
for (;;) {
|
|
218
266
|
const id = nextSessionId++;
|
|
219
267
|
const requestPath = `rr-session-${id}.json`;
|
|
220
268
|
try {
|
|
221
269
|
const handle = await fs.open(requestPath, "wx");
|
|
222
|
-
return { id, requestPath, responsePath: `rr-session-${id}.res.log`, handle };
|
|
270
|
+
return { id, requestPath, responsePath: `rr-session-${id}.res.log`, handle, overwrite: false };
|
|
223
271
|
} catch (error) {
|
|
224
272
|
if (isFileExistsError(error)) continue;
|
|
225
273
|
throw error;
|
package/src/utils/retry-after.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undef
|
|
|
28
28
|
return Math.max(...candidates);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function getHeadersFromError(error: unknown): HeadersLike {
|
|
31
|
+
export function getHeadersFromError(error: unknown): HeadersLike {
|
|
32
32
|
if (!error || typeof error !== "object") return undefined;
|
|
33
33
|
const record = error as { headers?: unknown; response?: { headers?: unknown }; cause?: unknown };
|
|
34
34
|
const direct = extractHeaders(record.headers) ?? extractHeaders(record.response?.headers);
|
package/src/utils/retry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
2
|
import { extractHttpStatusFromError, isRetryableError } from "@prometheus-ai/utils";
|
|
3
|
+
import { getHeadersFromError, getRetryAfterMsFromHeaders } from "./retry-after";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GitHub Copilot intermittently rejects preview models (gpt-5.3-codex,
|
|
@@ -24,6 +25,8 @@ export function isCopilotTransientModelError(error: unknown): boolean {
|
|
|
24
25
|
|
|
25
26
|
const COPILOT_MODEL_RETRY_MAX_ATTEMPTS = 3;
|
|
26
27
|
const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
|
|
28
|
+
/** Longest server-requested backoff we are willing to sit out before giving up. */
|
|
29
|
+
const COPILOT_RETRY_AFTER_MAX_WAIT_MS = 30_000;
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Wrap an initial Copilot request so transient `model_not_supported` 400s are
|
|
@@ -45,9 +48,27 @@ export async function callWithCopilotModelRetry<T>(
|
|
|
45
48
|
return await fn();
|
|
46
49
|
} catch (error) {
|
|
47
50
|
lastError = error;
|
|
48
|
-
|
|
51
|
+
// A latched abort (caller cancel or local watchdog) makes any retry a
|
|
52
|
+
// guaranteed-dead attempt — surface the original error, not the
|
|
53
|
+
// scheduler's AbortError.
|
|
54
|
+
if (options.signal?.aborted) throw error;
|
|
55
|
+
const transientModelError = isCopilotTransientModelError(error);
|
|
56
|
+
if (!transientModelError && !isRetryableError(error)) throw error;
|
|
49
57
|
if (attempt === COPILOT_MODEL_RETRY_MAX_ATTEMPTS - 1) break;
|
|
50
|
-
|
|
58
|
+
let delayMs = retryBaseDelayMs * (attempt + 1);
|
|
59
|
+
if (!transientModelError) {
|
|
60
|
+
const status = extractHttpStatusFromError(error);
|
|
61
|
+
if (status !== undefined) {
|
|
62
|
+
// Status-bearing retryable errors (429/5xx) are only re-sent when
|
|
63
|
+
// the server told us when to come back — a blind fixed-delay retry
|
|
64
|
+
// of a rate limit just burns the remaining attempts. Status-less
|
|
65
|
+
// transport blips (socket close, h2 reset) keep the linear backoff.
|
|
66
|
+
const retryAfterMs = getRetryAfterMsFromHeaders(getHeadersFromError(error));
|
|
67
|
+
if (retryAfterMs === undefined || retryAfterMs > COPILOT_RETRY_AFTER_MAX_WAIT_MS) throw error;
|
|
68
|
+
delayMs = Math.max(delayMs, retryAfterMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await scheduler.wait(delayMs, { signal: options.signal });
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
throw lastError;
|
|
@@ -37,8 +37,10 @@ When strict mode is requested (`strict=true` at call site), the schema MUST sati
|
|
|
37
37
|
3. **Object and tuple strictness is enforced recursively**
|
|
38
38
|
- Every object node gets `additionalProperties: false`.
|
|
39
39
|
- Every property key is included in `required`.
|
|
40
|
-
- Optional properties are
|
|
41
|
-
- `anyOf
|
|
40
|
+
- Optional properties are made nullable:
|
|
41
|
+
- Pure union nodes (only `anyOf` plus optional `description`) get a `{ "type": "null" }` branch appended in place — never a nested wrapper.
|
|
42
|
+
- All other nodes are wrapped as `anyOf: [<original schema>, { "type": "null" }]`. Nodes with constraining siblings next to `anyOf` MUST keep the wrapper: sibling keywords are conjunctive with `anyOf`, so appending a null branch would not make the node nullable.
|
|
43
|
+
- Nested pure unions are spliced into the parent `anyOf` (`(A ∨ B) ∨ C` → `A ∨ B ∨ C`); an inner `description` is hoisted to the parent when the parent has none. Strict output MUST NOT contain an `anyOf` branch that is itself a pure union — some upstream validators (OpenRouter DeepSeek) reject branches without `type`.
|
|
42
44
|
- Tuple entries in `prefixItems` are strictified recursively.
|
|
43
45
|
|
|
44
46
|
4. **Schema nodes must be representable in strict mode**
|
|
@@ -154,6 +154,22 @@ export const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record<string, Record<string,
|
|
|
154
154
|
null: {},
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Flat set of every type-specific key across all CCA types.
|
|
159
|
+
* Used to identify sibling keys that need filtering during mixed-type collapse.
|
|
160
|
+
*/
|
|
161
|
+
export const ALL_CCA_TYPE_SPECIFIC_KEYS: Record<string, true> = buildAllCcaTypeSpecificKeys();
|
|
162
|
+
|
|
163
|
+
function buildAllCcaTypeSpecificKeys(): Record<string, true> {
|
|
164
|
+
const all: Record<string, true> = {};
|
|
165
|
+
for (const typeKeys of Object.values(CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS)) {
|
|
166
|
+
for (const key in typeKeys) {
|
|
167
|
+
all[key] = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return all;
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
/**
|
|
158
174
|
* Cloud Code Assist shared schema keys allowed on any type.
|
|
159
175
|
* Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing.
|
|
@@ -20,6 +20,14 @@ export interface JsonSchemaValidationIssue {
|
|
|
20
20
|
message: string;
|
|
21
21
|
expectedTypes?: string[];
|
|
22
22
|
keyword?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Marks issues that originate inside a failed `anyOf` / `oneOf` branch.
|
|
25
|
+
* Consumers such as the tool-argument coercion layer use this to avoid
|
|
26
|
+
* applying type repairs (e.g. singleton-array wrapping) that would be
|
|
27
|
+
* authoritative outside of a combinator but are only one candidate
|
|
28
|
+
* branch's expectation here.
|
|
29
|
+
*/
|
|
30
|
+
fromUnionBranch?: boolean;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export interface JsonSchemaValidationResult {
|
|
@@ -242,7 +250,17 @@ function validateSchemaNode(
|
|
|
242
250
|
const branchValid = keyword === "anyOf" ? matches > 0 : matches === 1;
|
|
243
251
|
if (!branchValid) {
|
|
244
252
|
if (matches === 0 && firstIssues && firstIssues.length > 0) {
|
|
245
|
-
issues
|
|
253
|
+
// Only tag issues that sit at the combinator's own path as
|
|
254
|
+
// union-branch; deeper issues describe a specific field within
|
|
255
|
+
// the failed branch and should remain individually repairable.
|
|
256
|
+
const unionDepth = path.length;
|
|
257
|
+
for (const branchIssue of firstIssues) {
|
|
258
|
+
if (branchIssue.path.length === unionDepth) {
|
|
259
|
+
issues.push({ ...branchIssue, fromUnionBranch: true });
|
|
260
|
+
} else {
|
|
261
|
+
issues.push(branchIssue);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
246
264
|
} else {
|
|
247
265
|
pushIssue(
|
|
248
266
|
issues,
|
|
@@ -11,6 +11,7 @@ import { dereferenceJsonSchema } from "./dereference";
|
|
|
11
11
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
12
12
|
import { areJsonValuesEqual, mergePropertySchemas } from "./equality";
|
|
13
13
|
import {
|
|
14
|
+
ALL_CCA_TYPE_SPECIFIC_KEYS,
|
|
14
15
|
CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS,
|
|
15
16
|
CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS,
|
|
16
17
|
COMBINATOR_KEYS,
|
|
@@ -51,7 +52,6 @@ export interface NormalizeSchemaOptions {
|
|
|
51
52
|
|
|
52
53
|
interface NormalizeSchemaWalkOptions extends NormalizeSchemaOptions {
|
|
53
54
|
insideProperties: boolean;
|
|
54
|
-
epoch: number;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
interface ResidualIncompatibilityChecks {
|
|
@@ -218,13 +218,27 @@ function applyDescriptionSpill(
|
|
|
218
218
|
|
|
219
219
|
function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions): unknown {
|
|
220
220
|
if (Array.isArray(value)) {
|
|
221
|
-
if (!
|
|
222
|
-
|
|
221
|
+
if (!enter(value)) return [];
|
|
222
|
+
try {
|
|
223
|
+
return value.map(entry => normalizeSchemaNode(entry, options));
|
|
224
|
+
} finally {
|
|
225
|
+
exit(value);
|
|
226
|
+
}
|
|
223
227
|
}
|
|
224
228
|
if (!isJsonObject(value)) {
|
|
225
229
|
return value;
|
|
226
230
|
}
|
|
227
|
-
|
|
231
|
+
// `enter`/`exit` path-tracking (not a visited-set): DAG-shared subtrees are
|
|
232
|
+
// normalized at every occurrence; only true cycles short-circuit to `{}`.
|
|
233
|
+
if (!enter(value)) return {};
|
|
234
|
+
try {
|
|
235
|
+
return normalizeSchemaObjectNode(value, options);
|
|
236
|
+
} finally {
|
|
237
|
+
exit(value);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeSchemaObjectNode(value: JsonObject, options: NormalizeSchemaWalkOptions): unknown {
|
|
228
242
|
let obj = options.normalizeFieldNames && !options.insideProperties ? applySnakeCaseRenames(value) : value;
|
|
229
243
|
if (options.collapseNullFields && !options.insideProperties) {
|
|
230
244
|
obj = preHandleNullFields(obj);
|
|
@@ -501,12 +515,32 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
|
|
|
501
515
|
if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
|
|
502
516
|
return schema;
|
|
503
517
|
}
|
|
504
|
-
|
|
505
518
|
const nextSchema = copySchemaWithout(schema, combiner);
|
|
506
519
|
const nonNullTypes = variantTypes.filter(t => t !== "null");
|
|
507
|
-
|
|
520
|
+
const chosenType: string = nonNullTypes[0] ?? variantTypes[0];
|
|
521
|
+
nextSchema.type = chosenType;
|
|
522
|
+
const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[chosenType] ?? {};
|
|
523
|
+
|
|
524
|
+
// Strip sibling keys that were copied from the parent and belong to a
|
|
525
|
+
// different type (e.g. `items` sibling on a now-string-typed schema).
|
|
526
|
+
for (const key in nextSchema) {
|
|
527
|
+
if (!Object.hasOwn(nextSchema, key)) continue;
|
|
528
|
+
if (key === "type") continue;
|
|
529
|
+
if (
|
|
530
|
+
Object.hasOwn(ALL_CCA_TYPE_SPECIFIC_KEYS, key) &&
|
|
531
|
+
!Object.hasOwn(chosenTypeAllowedKeys, key) &&
|
|
532
|
+
!Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)
|
|
533
|
+
) {
|
|
534
|
+
delete nextSchema[key];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
508
538
|
for (const key in mergedVariantFields) {
|
|
509
539
|
if (!Object.hasOwn(mergedVariantFields, key)) continue;
|
|
540
|
+
// Drop type-specific keys that don't belong to the chosen type
|
|
541
|
+
if (!Object.hasOwn(chosenTypeAllowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
510
544
|
const value = mergedVariantFields[key];
|
|
511
545
|
const existingValue = nextSchema[key];
|
|
512
546
|
if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
|
|
@@ -774,7 +808,6 @@ export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions)
|
|
|
774
808
|
let normalized = normalizeSchemaNode(dereferenced, {
|
|
775
809
|
...options,
|
|
776
810
|
insideProperties: false,
|
|
777
|
-
epoch: epochNext(),
|
|
778
811
|
});
|
|
779
812
|
if (options.stripResidualCombinersFixpoint) {
|
|
780
813
|
normalized = stripResidualCombiners(normalized);
|
|
@@ -1069,7 +1102,7 @@ function inferStrictPrimitiveTypeFromEnumOrConst(node: Record<string, unknown>):
|
|
|
1069
1102
|
* so repeated calls (different providers, retries, batching) reuse the same
|
|
1070
1103
|
* computed pair without re-walking the tree.
|
|
1071
1104
|
*/
|
|
1072
|
-
const kStrictSchema = Symbol("
|
|
1105
|
+
const kStrictSchema = Symbol("prometheus.schema.strict");
|
|
1073
1106
|
|
|
1074
1107
|
/**
|
|
1075
1108
|
* Detect schemas that strict mode *cannot* represent.
|
|
@@ -1404,6 +1437,21 @@ export function sanitizeSchemaForStrictMode(
|
|
|
1404
1437
|
return sanitized;
|
|
1405
1438
|
}
|
|
1406
1439
|
|
|
1440
|
+
/**
|
|
1441
|
+
* A node whose only constraining keyword is `anyOf` (annotations like
|
|
1442
|
+
* `description` aside). Only such nodes can be merged into an enclosing
|
|
1443
|
+
* union without changing semantics: sibling keywords (`type`, `enum`,
|
|
1444
|
+
* `properties`, …) apply conjunctively with `anyOf`, so spreading the
|
|
1445
|
+
* branches of a non-pure node would drop those constraints.
|
|
1446
|
+
*/
|
|
1447
|
+
function isPureAnyOfNode(value: unknown): value is Record<string, unknown> & { anyOf: unknown[] } {
|
|
1448
|
+
if (!isJsonObject(value) || !Array.isArray(value.anyOf)) return false;
|
|
1449
|
+
for (const key in value) {
|
|
1450
|
+
if (key !== "anyOf" && key !== "description") return false;
|
|
1451
|
+
}
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1407
1455
|
/**
|
|
1408
1456
|
* Recursively enforces JSON Schema constraints required by OpenAI/Codex strict mode:
|
|
1409
1457
|
* - `additionalProperties: false` on every object node
|
|
@@ -1472,6 +1520,10 @@ function enforceStrictSchemaBody(
|
|
|
1472
1520
|
strictProperties[key] = processed;
|
|
1473
1521
|
continue;
|
|
1474
1522
|
}
|
|
1523
|
+
if (isPureAnyOfNode(processed)) {
|
|
1524
|
+
strictProperties[key] = { ...processed, anyOf: [...processed.anyOf, { type: "null" }] };
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1475
1527
|
if (isJsonObject(processed) && typeof processed.description === "string") {
|
|
1476
1528
|
const { description, ...withoutDescription } = processed;
|
|
1477
1529
|
strictProperties[key] = { anyOf: [withoutDescription, { type: "null" }], description };
|
|
@@ -1512,6 +1564,26 @@ function enforceStrictSchemaBody(
|
|
|
1512
1564
|
);
|
|
1513
1565
|
}
|
|
1514
1566
|
}
|
|
1567
|
+
// Splice nested pure unions into the parent `anyOf`: `(A ∨ B) ∨ C` ≡ `A ∨ B ∨ C`.
|
|
1568
|
+
// Some strict-mode validators (e.g. DeepSeek behind OpenRouter) reject anyOf
|
|
1569
|
+
// branches that carry no `type`, which is exactly what a nested combinator
|
|
1570
|
+
// node looks like (#2270). Branch recursion above already flattened deeper
|
|
1571
|
+
// levels bottom-up, so a single pass suffices.
|
|
1572
|
+
if (Array.isArray(result.anyOf) && result.anyOf.some(isPureAnyOfNode)) {
|
|
1573
|
+
const flattened: unknown[] = [];
|
|
1574
|
+
for (const branch of result.anyOf) {
|
|
1575
|
+
if (!isPureAnyOfNode(branch)) {
|
|
1576
|
+
flattened.push(branch);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
flattened.push(...branch.anyOf);
|
|
1580
|
+
// Keep the inner annotation when the parent has none.
|
|
1581
|
+
if (typeof branch.description === "string" && result.description === undefined) {
|
|
1582
|
+
result.description = branch.description;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
result.anyOf = flattened;
|
|
1586
|
+
}
|
|
1515
1587
|
for (const defsKey of ["$defs", "definitions"] as const) {
|
|
1516
1588
|
if (result[defsKey] != null && typeof result[defsKey] === "object" && !Array.isArray(result[defsKey])) {
|
|
1517
1589
|
const defs = result[defsKey] as Record<string, unknown>;
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Caveats: the stamp lives as long as the host object, even after callers
|
|
11
11
|
* release their references to the cached value — only use this for caches
|
|
12
|
-
* whose lifetime should match the host. Frozen hosts
|
|
13
|
-
*
|
|
12
|
+
* whose lifetime should match the host. Frozen hosts cannot be stamped;
|
|
13
|
+
* `define` silently skips them, so memoization/visit-tracking degrades to
|
|
14
|
+
* best-effort (recompute on every call, no cycle protection) instead of
|
|
15
|
+
* throwing.
|
|
14
16
|
*/
|
|
15
|
-
|
|
16
17
|
function define<T extends object>(target: T, key: symbol, value: unknown): void {
|
|
18
|
+
if (Object.isFrozen(target)) return;
|
|
17
19
|
Object.defineProperty(target, key, { value, writable: true, configurable: true });
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -38,7 +40,7 @@ export function stamp<T extends object, V>(target: T, key: symbol, compute: (tar
|
|
|
38
40
|
* for (const child of node.children) walk(child, epoch);
|
|
39
41
|
* }
|
|
40
42
|
*/
|
|
41
|
-
const kEpoch = Symbol("
|
|
43
|
+
const kEpoch = Symbol("prometheus.schema.epoch");
|
|
42
44
|
let __epoch = 0;
|
|
43
45
|
|
|
44
46
|
export function epochNext(): number {
|
|
@@ -77,9 +79,15 @@ export function once<T extends object>(target: T, epoch: number): boolean {
|
|
|
77
79
|
* finally { exit(node); }
|
|
78
80
|
* }
|
|
79
81
|
*/
|
|
80
|
-
const kDepth = Symbol("
|
|
82
|
+
const kDepth = Symbol("prometheus.schema.depth");
|
|
81
83
|
|
|
82
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Returns `true` on first entry, `false` if `target` is already on the
|
|
86
|
+
* current path. A `false` return does NOT deepen the counter — callers pair
|
|
87
|
+
* `exit` only with successful enters (`if (!enter(n)) bail; try {…} finally
|
|
88
|
+
* { exit(n); }`), so incrementing on the cycle branch would leak depth and
|
|
89
|
+
* make every later top-level walk of the same object misreport a cycle.
|
|
90
|
+
*/
|
|
83
91
|
export function enter<T extends object>(target: T): boolean {
|
|
84
92
|
const slot = target as Record<symbol, number | undefined>;
|
|
85
93
|
const cur = slot[kDepth];
|
|
@@ -87,11 +95,15 @@ export function enter<T extends object>(target: T): boolean {
|
|
|
87
95
|
define(target, kDepth, 1);
|
|
88
96
|
return true;
|
|
89
97
|
}
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
if (cur !== 0) return false;
|
|
99
|
+
slot[kDepth] = 1;
|
|
100
|
+
return true;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function exit<T extends object>(target: T): void {
|
|
95
|
-
const slot = target as Record<symbol, number>;
|
|
96
|
-
slot[kDepth]
|
|
104
|
+
const slot = target as Record<symbol, number | undefined>;
|
|
105
|
+
const cur = slot[kDepth];
|
|
106
|
+
// Frozen targets never received the kDepth stamp in `enter` — nothing to unwind.
|
|
107
|
+
if (cur === undefined) return;
|
|
108
|
+
slot[kDepth] = cur - 1;
|
|
97
109
|
}
|