jeopi-ai 16.2.13
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 +4347 -0
- package/README.md +1193 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +73 -0
- package/dist/types/auth-broker/discover.d.ts +35 -0
- package/dist/types/auth-broker/index.d.ts +7 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +102 -0
- package/dist/types/auth-broker/server.d.ts +43 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
- package/dist/types/auth-broker/types.d.ts +107 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +36 -0
- package/dist/types/auth-gateway/types.d.ts +123 -0
- package/dist/types/auth-retry.d.ts +124 -0
- package/dist/types/auth-storage.d.ts +1026 -0
- package/dist/types/dialect/anthropic.d.ts +15 -0
- package/dist/types/dialect/catalog.d.ts +3 -0
- package/dist/types/dialect/coercion.d.ts +23 -0
- package/dist/types/dialect/deepseek.d.ts +14 -0
- package/dist/types/dialect/demotion.d.ts +23 -0
- package/dist/types/dialect/examples.d.ts +2 -0
- package/dist/types/dialect/factory.d.ts +3 -0
- package/dist/types/dialect/fenced-thinking.d.ts +53 -0
- package/dist/types/dialect/gemini.d.ts +17 -0
- package/dist/types/dialect/gemma.d.ts +15 -0
- package/dist/types/dialect/glm.d.ts +9 -0
- package/dist/types/dialect/harmony.d.ts +8 -0
- package/dist/types/dialect/hermes.d.ts +9 -0
- package/dist/types/dialect/history.d.ts +3 -0
- package/dist/types/dialect/index.d.ts +11 -0
- package/dist/types/dialect/inventory.d.ts +12 -0
- package/dist/types/dialect/kimi.d.ts +14 -0
- package/dist/types/dialect/minimax.d.ts +3 -0
- package/dist/types/dialect/owned-stream.d.ts +4 -0
- package/dist/types/dialect/qwen3.d.ts +9 -0
- package/dist/types/dialect/rendering.d.ts +45 -0
- package/dist/types/dialect/thinking.d.ts +6 -0
- package/dist/types/dialect/types.d.ts +69 -0
- package/dist/types/dialect/xml.d.ts +9 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +79 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/error/rate-limit.d.ts +59 -0
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
- package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
- package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
- package/dist/types/providers/amazon-bedrock.d.ts +39 -0
- package/dist/types/providers/anthropic-client.d.ts +94 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic-wire.d.ts +318 -0
- package/dist/types/providers/anthropic.d.ts +248 -0
- package/dist/types/providers/aws-credentials.d.ts +53 -0
- package/dist/types/providers/aws-eventstream.d.ts +39 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +16 -0
- package/dist/types/providers/cursor.d.ts +91 -0
- package/dist/types/providers/devin.d.ts +12 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +32 -0
- package/dist/types/providers/google-gemini-cli.d.ts +118 -0
- package/dist/types/providers/google-interactions.d.ts +65 -0
- package/dist/types/providers/google-shared.d.ts +203 -0
- package/dist/types/providers/google-types.d.ts +155 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +178 -0
- package/dist/types/providers/ollama.d.ts +7 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-chat-wire.d.ts +644 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
- package/dist/types/providers/openai-codex-responses.d.ts +108 -0
- package/dist/types/providers/openai-completions.d.ts +45 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
- package/dist/types/providers/openai-responses.d.ts +126 -0
- package/dist/types/providers/openai-shared.d.ts +506 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +69 -0
- package/dist/types/providers/register-builtins.d.ts +32 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +11 -0
- package/dist/types/providers/vision-guard.d.ts +20 -0
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/registry/api-key-login.d.ts +42 -0
- package/dist/types/registry/api-key-validation.d.ts +43 -0
- package/dist/types/registry/azure.d.ts +4 -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/coreweave.d.ts +7 -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/devin.d.ts +8 -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-workflow.d.ts +10 -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/llama-cpp.d.ts +8 -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/registry/oauth/anthropic.d.ts +23 -0
- package/dist/types/registry/oauth/callback-server.d.ts +72 -0
- package/dist/types/registry/oauth/cursor.d.ts +15 -0
- package/dist/types/registry/oauth/devin.d.ts +5 -0
- package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
- package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
- package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
- package/dist/types/registry/oauth/index.d.ts +64 -0
- package/dist/types/registry/oauth/kimi.d.ts +21 -0
- package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
- package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
- package/dist/types/registry/oauth/opencode.d.ts +18 -0
- package/dist/types/registry/oauth/perplexity.d.ts +9 -0
- package/dist/types/registry/oauth/pkce.d.ts +8 -0
- package/dist/types/registry/oauth/types.d.ts +56 -0
- package/dist/types/registry/oauth/wafer.d.ts +1 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
- package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
- 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 +303 -0
- package/dist/types/registry/sakana.d.ts +7 -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/umans.d.ts +7 -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-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 +44 -0
- package/dist/types/types.d.ts +715 -0
- package/dist/types/usage/claude.d.ts +4 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +15 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/ollama.d.ts +5 -0
- package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
- package/dist/types/usage/openai-codex-reset.d.ts +79 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/opencode-go.d.ts +2 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +346 -0
- package/dist/types/utils/abort.d.ts +25 -0
- package/dist/types/utils/anthropic-auth.d.ts +35 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/empty-completion-retry.d.ts +21 -0
- package/dist/types/utils/event-stream.d.ts +30 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/google-validation.d.ts +2 -0
- package/dist/types/utils/harmony-leak.d.ts +118 -0
- package/dist/types/utils/http-inspector.d.ts +30 -0
- package/dist/types/utils/idle-iterator.d.ts +137 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
- package/dist/types/utils/openai-http.d.ts +54 -0
- package/dist/types/utils/openrouter-headers.d.ts +1 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/proxy.d.ts +29 -0
- package/dist/types/utils/request-debug.d.ts +29 -0
- package/dist/types/utils/retry-after.d.ts +4 -0
- package/dist/types/utils/retry.d.ts +14 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +54 -0
- package/dist/types/utils/schema/index.d.ts +15 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +124 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +17 -0
- package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/typescript.d.ts +18 -0
- package/dist/types/utils/schema/wire.d.ts +92 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
- package/dist/types/utils/sse-debug.d.ts +5 -0
- package/dist/types/utils/stream-markup-healing.d.ts +87 -0
- package/dist/types/utils/thinking-loop.d.ts +102 -0
- package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +42 -0
- package/dist/types/utils.d.ts +24 -0
- package/package.json +139 -0
- package/src/api-registry.ts +109 -0
- package/src/auth-broker/client.ts +359 -0
- package/src/auth-broker/discover.ts +222 -0
- package/src/auth-broker/index.ts +7 -0
- package/src/auth-broker/refresher.ts +117 -0
- package/src/auth-broker/remote-store.ts +657 -0
- package/src/auth-broker/server.ts +646 -0
- package/src/auth-broker/snapshot-cache.ts +191 -0
- package/src/auth-broker/types.ts +130 -0
- package/src/auth-broker/wire-schemas.ts +249 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +802 -0
- package/src/auth-gateway/types.ts +151 -0
- package/src/auth-retry.ts +250 -0
- package/src/auth-storage.ts +5576 -0
- package/src/dialect/anthropic.md +31 -0
- package/src/dialect/anthropic.ts +608 -0
- package/src/dialect/catalog.ts +29 -0
- package/src/dialect/coercion.ts +136 -0
- package/src/dialect/deepseek.md +24 -0
- package/src/dialect/deepseek.ts +609 -0
- package/src/dialect/demotion.ts +36 -0
- package/src/dialect/examples.ts +33 -0
- package/src/dialect/factory.ts +34 -0
- package/src/dialect/fenced-thinking.ts +184 -0
- package/src/dialect/gemini.md +44 -0
- package/src/dialect/gemini.ts +597 -0
- package/src/dialect/gemma.md +33 -0
- package/src/dialect/gemma.ts +387 -0
- package/src/dialect/glm.md +32 -0
- package/src/dialect/glm.ts +456 -0
- package/src/dialect/harmony.md +31 -0
- package/src/dialect/harmony.ts +346 -0
- package/src/dialect/hermes.md +25 -0
- package/src/dialect/hermes.ts +206 -0
- package/src/dialect/history.ts +81 -0
- package/src/dialect/index.ts +15 -0
- package/src/dialect/inventory.ts +73 -0
- package/src/dialect/kimi.md +24 -0
- package/src/dialect/kimi.ts +340 -0
- package/src/dialect/minimax.md +31 -0
- package/src/dialect/minimax.ts +95 -0
- package/src/dialect/owned-stream.ts +470 -0
- package/src/dialect/prompt-template.md +12 -0
- package/src/dialect/qwen3.md +28 -0
- package/src/dialect/qwen3.ts +240 -0
- package/src/dialect/rendering.ts +249 -0
- package/src/dialect/thinking.ts +122 -0
- package/src/dialect/types.ts +57 -0
- package/src/dialect/xml.md +22 -0
- package/src/dialect/xml.ts +90 -0
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +506 -0
- package/src/error/format.ts +45 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +62 -0
- package/src/error/rate-limit.ts +161 -0
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +49 -0
- package/src/provider-details.ts +90 -0
- package/src/providers/__tests__/google-auth.test.ts +144 -0
- package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
- package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
- package/src/providers/amazon-bedrock.ts +1042 -0
- package/src/providers/anthropic-client.ts +295 -0
- package/src/providers/anthropic-messages-server-schema.ts +252 -0
- package/src/providers/anthropic-messages-server.ts +756 -0
- package/src/providers/anthropic-wire.ts +318 -0
- package/src/providers/anthropic.ts +4078 -0
- package/src/providers/aws-credentials.ts +586 -0
- package/src/providers/aws-eventstream.ts +181 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +382 -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 +2695 -0
- package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
- package/src/providers/devin/proto/buf.gen.yaml +33 -0
- package/src/providers/devin/proto/buf.yaml +17 -0
- package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
- package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
- package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
- package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
- package/src/providers/devin/proto/cel/expr/value.proto +41 -0
- package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
- package/src/providers/devin/proto/errorspb/errors.proto +56 -0
- package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
- package/src/providers/devin/proto/errorspb/markers.proto +10 -0
- package/src/providers/devin/proto/errorspb/tags.proto +12 -0
- package/src/providers/devin/proto/errorspb/testing.proto +6 -0
- package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
- package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
- package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
- package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
- package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
- package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
- package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
- package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
- package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
- package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
- package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
- package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
- package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
- package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
- package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
- package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
- package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
- package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
- package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
- package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
- package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
- package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
- package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
- package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
- package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
- package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
- package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
- package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
- package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
- package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
- package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
- package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
- package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
- package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
- package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
- package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
- package/src/providers/devin.ts +577 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +141 -0
- package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
- package/src/providers/gitlab-duo-workflow.ts +3058 -0
- package/src/providers/gitlab-duo.ts +395 -0
- package/src/providers/google-auth.ts +350 -0
- package/src/providers/google-gemini-cli.ts +1362 -0
- package/src/providers/google-interactions.ts +753 -0
- package/src/providers/google-shared.ts +1103 -0
- package/src/providers/google-types.ts +180 -0
- package/src/providers/google-vertex.ts +183 -0
- package/src/providers/google.ts +87 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +507 -0
- package/src/providers/ollama.ts +773 -0
- package/src/providers/openai-anthropic-shim.ts +152 -0
- package/src/providers/openai-chat-server-schema.ts +242 -0
- package/src/providers/openai-chat-server.ts +715 -0
- package/src/providers/openai-chat-wire.ts +847 -0
- package/src/providers/openai-codex/request-transformer.ts +295 -0
- package/src/providers/openai-codex/response-handler.ts +102 -0
- package/src/providers/openai-codex-responses.ts +3468 -0
- package/src/providers/openai-completions.ts +2173 -0
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses-reasoning-suppression.md +1 -0
- package/src/providers/openai-responses-server-schema.ts +282 -0
- package/src/providers/openai-responses-server.ts +1280 -0
- package/src/providers/openai-responses-wire.ts +6391 -0
- package/src/providers/openai-responses.ts +1022 -0
- package/src/providers/openai-shared.ts +2648 -0
- package/src/providers/pi-native-client.ts +266 -0
- package/src/providers/pi-native-server.ts +242 -0
- package/src/providers/register-builtins.ts +475 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +787 -0
- package/src/providers/vision-guard.ts +54 -0
- package/src/registry/aimlapi.ts +6 -0
- package/src/registry/alibaba-coding-plan.ts +95 -0
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/registry/api-key-login.ts +112 -0
- package/src/registry/api-key-validation.ts +161 -0
- package/src/registry/azure.ts +6 -0
- package/src/registry/cerebras.ts +23 -0
- package/src/registry/cloudflare-ai-gateway.ts +45 -0
- package/src/registry/coreweave.ts +40 -0
- package/src/registry/cursor.ts +20 -0
- package/src/registry/deepseek.ts +46 -0
- package/src/registry/derived.ts +9 -0
- package/src/registry/devin.ts +15 -0
- package/src/registry/firepass.ts +32 -0
- package/src/registry/fireworks.ts +28 -0
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo-workflow.ts +20 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/google-antigravity.ts +22 -0
- package/src/registry/google-gemini-cli.ts +22 -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/registry/huggingface.ts +29 -0
- package/src/registry/index.ts +4 -0
- package/src/registry/kagi.ts +46 -0
- package/src/registry/kilo.ts +114 -0
- package/src/registry/kimi-code.ts +17 -0
- package/src/registry/litellm.ts +45 -0
- package/src/registry/llama-cpp.ts +35 -0
- package/src/registry/lm-studio.ts +31 -0
- 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/registry/moonshot.ts +22 -0
- package/src/registry/nanogpt.ts +22 -0
- package/src/registry/nvidia.ts +61 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
- package/src/registry/oauth/anthropic.ts +311 -0
- package/src/registry/oauth/callback-server.ts +315 -0
- package/src/registry/oauth/cursor.ts +171 -0
- package/src/registry/oauth/devin.ts +124 -0
- package/src/registry/oauth/github-copilot.ts +369 -0
- package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
- package/src/registry/oauth/gitlab-duo.ts +222 -0
- package/src/registry/oauth/google-antigravity.ts +209 -0
- package/src/registry/oauth/google-gemini-cli.ts +273 -0
- package/src/registry/oauth/google-oauth-shared.ts +125 -0
- package/src/registry/oauth/index.ts +269 -0
- package/src/registry/oauth/kimi.ts +289 -0
- package/src/registry/oauth/minimax-code.ts +53 -0
- package/src/registry/oauth/oauth.html +311 -0
- package/src/registry/oauth/openai-codex.ts +364 -0
- package/src/registry/oauth/opencode.ts +50 -0
- package/src/registry/oauth/perplexity.ts +228 -0
- package/src/registry/oauth/pkce.ts +18 -0
- package/src/registry/oauth/types.ts +65 -0
- package/src/registry/oauth/wafer.ts +24 -0
- package/src/registry/oauth/xai-oauth.ts +394 -0
- package/src/registry/oauth/xiaomi.ts +211 -0
- package/src/registry/ollama-cloud.ts +36 -0
- package/src/registry/ollama.ts +43 -0
- 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/registry/openrouter.ts +28 -0
- package/src/registry/parallel.ts +45 -0
- package/src/registry/perplexity.ts +13 -0
- package/src/registry/qianfan.ts +27 -0
- package/src/registry/qwen-portal.ts +50 -0
- package/src/registry/registry.ts +161 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/synthetic.ts +21 -0
- package/src/registry/tavily.ts +45 -0
- package/src/registry/together.ts +22 -0
- package/src/registry/types.ts +56 -0
- package/src/registry/umans.ts +23 -0
- package/src/registry/venice.ts +33 -0
- package/src/registry/vercel-ai-gateway.ts +38 -0
- package/src/registry/vllm.ts +34 -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/registry/zai.ts +27 -0
- package/src/registry/zenmux.ts +22 -0
- package/src/registry/zhipu-coding-plan.ts +27 -0
- package/src/stream.ts +1778 -0
- package/src/types.ts +856 -0
- package/src/usage/claude.ts +485 -0
- package/src/usage/gemini.ts +258 -0
- package/src/usage/github-copilot.ts +424 -0
- package/src/usage/google-antigravity.ts +497 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +30 -0
- package/src/usage/ollama.ts +41 -0
- package/src/usage/openai-codex-base-url.ts +35 -0
- package/src/usage/openai-codex-reset.ts +174 -0
- package/src/usage/openai-codex.ts +535 -0
- package/src/usage/opencode-go.ts +89 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +321 -0
- package/src/usage.ts +333 -0
- package/src/utils/abort.ts +67 -0
- package/src/utils/anthropic-auth.ts +93 -0
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/empty-completion-retry.ts +159 -0
- package/src/utils/event-stream.ts +171 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/google-validation.ts +25 -0
- package/src/utils/harmony-leak.ts +456 -0
- package/src/utils/http-inspector.ts +168 -0
- package/src/utils/idle-iterator.ts +473 -0
- package/src/utils/leaked-thinking-stream.ts +294 -0
- package/src/utils/openai-http.ts +122 -0
- package/src/utils/openrouter-headers.ts +12 -0
- package/src/utils/parse-bind.ts +56 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/proxy.ts +240 -0
- package/src/utils/request-debug.ts +351 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +59 -0
- package/src/utils/schema/CONSTRAINTS.md +166 -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 +207 -0
- package/src/utils/schema/index.ts +15 -0
- package/src/utils/schema/json-schema-validator.ts +595 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1901 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +109 -0
- package/src/utils/schema/strict-tool-validation.ts +117 -0
- package/src/utils/schema/types.ts +10 -0
- package/src/utils/schema/typescript.ts +198 -0
- package/src/utils/schema/wire.ts +789 -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 +18 -0
- package/src/utils/stream-markup-healing.ts +247 -0
- package/src/utils/thinking-loop.ts +552 -0
- package/src/utils/tool-call-loop-guard.ts +107 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1507 -0
- package/src/utils.ts +171 -0
|
@@ -0,0 +1,1507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-call argument validation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Tools may declare their parameters as either Zod schemas (canonical) or
|
|
5
|
+
* plain JSON Schema (legacy / extensions). This module is the single
|
|
6
|
+
* entrypoint the agent calls before dispatching a tool — it:
|
|
7
|
+
*
|
|
8
|
+
* 1. Builds (or fetches from cache) a `ValidationContext` for the tool —
|
|
9
|
+
* the Zod schema if available plus the equivalent wire JSON Schema, or
|
|
10
|
+
* just the JSON Schema for non-Zod tools.
|
|
11
|
+
* 2. Normalizes LLM quirks (null / "null" → omit-or-default substitution)
|
|
12
|
+
* against the JSON Schema before validation.
|
|
13
|
+
* 3. Validates with the Zod or JSON-Schema validator.
|
|
14
|
+
* 4. On failure, walks the resulting issues and coerces common LLM type
|
|
15
|
+
* drift (JSON-stringified values, boolean/number/string scalar drift),
|
|
16
|
+
* drops unrecognized keys, and retries up to `MAX_COERCION_PASSES` times.
|
|
17
|
+
* 5. Throws a formatted error if reconciliation fails; otherwise returns
|
|
18
|
+
* the parsed arguments with original unknown root fields preserved (so
|
|
19
|
+
* hallucinated top-level keys still surface to the caller).
|
|
20
|
+
*
|
|
21
|
+
* The goal is to be conservative: every coercion is a structural rewrite that
|
|
22
|
+
* keeps the schema in charge of acceptance — we never invent values, only
|
|
23
|
+
* massage shapes the LLM almost got right.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type Type, type } from "arktype";
|
|
27
|
+
import { structuredCloneJSON } from "jeopi-utils";
|
|
28
|
+
import type { ZodType } from "zod/v4";
|
|
29
|
+
import type { $ZodIssue as ZodIssue } from "zod/v4/core";
|
|
30
|
+
import * as AIError from "../error";
|
|
31
|
+
import type { Tool, ToolCall } from "../types";
|
|
32
|
+
import { upgradeJsonSchemaTo202012 } from "./schema/draft";
|
|
33
|
+
import {
|
|
34
|
+
isJsonSchemaValueValid,
|
|
35
|
+
type JsonSchemaValidationIssue,
|
|
36
|
+
validateJsonSchemaValue,
|
|
37
|
+
} from "./schema/json-schema-validator";
|
|
38
|
+
import { stamp } from "./schema/stamps";
|
|
39
|
+
import { arkToWireSchema, isArkSchema, isZodSchema, zodToWireSchema } from "./schema/wire";
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Type Coercion Utilities
|
|
43
|
+
// ============================================================================
|
|
44
|
+
//
|
|
45
|
+
// LLMs sometimes produce tool arguments where a value has the right meaning but
|
|
46
|
+
// the wrong JSON type. For example, an array parameter might arrive as
|
|
47
|
+
// `"[1, 2, 3]"`, a boolean as `"yes"` or `1`, or a string field as a structured
|
|
48
|
+
// object that should be embedded verbatim.
|
|
49
|
+
//
|
|
50
|
+
// Rather than rejecting these outright, we attempt automatic coercion:
|
|
51
|
+
// 1. Validate against the tool's schema (Zod, derived from TypeBox when the
|
|
52
|
+
// tool was authored with TypeBox).
|
|
53
|
+
// 2. For each type error, perform only the schema-directed rewrite that
|
|
54
|
+
// matches the expected type.
|
|
55
|
+
// 3. Re-validate the full argument object after each coercion pass.
|
|
56
|
+
//
|
|
57
|
+
// This is intentionally conservative: each rewrite is small and validation
|
|
58
|
+
// remains the source of truth for whether the result is accepted.
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/** Regex matching valid JSON number literals (integers, decimals, scientific notation) */
|
|
62
|
+
const JSON_NUMBER_PATTERN = /^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
63
|
+
|
|
64
|
+
/** Regex matching numeric strings (allows leading zeros) */
|
|
65
|
+
const NUMERIC_STRING_PATTERN = /^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if a value matches any of the expected JSON Schema types.
|
|
69
|
+
* Used to verify that a parsed JSON value is actually what the schema wants.
|
|
70
|
+
*/
|
|
71
|
+
function matchesExpectedType(value: unknown, expectedTypes: string[]): boolean {
|
|
72
|
+
return expectedTypes.some(type => {
|
|
73
|
+
switch (type) {
|
|
74
|
+
case "string":
|
|
75
|
+
return typeof value === "string";
|
|
76
|
+
case "number":
|
|
77
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
78
|
+
case "integer":
|
|
79
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
80
|
+
case "boolean":
|
|
81
|
+
return typeof value === "boolean";
|
|
82
|
+
case "null":
|
|
83
|
+
return value === null;
|
|
84
|
+
case "array":
|
|
85
|
+
return Array.isArray(value);
|
|
86
|
+
case "object":
|
|
87
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
88
|
+
default:
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tryParseNumberString(value: string, expectedTypes: string[]): { value: unknown; changed: boolean } {
|
|
95
|
+
if (!expectedTypes.includes("number") && !expectedTypes.includes("integer")) {
|
|
96
|
+
return { value, changed: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (!trimmed || !NUMERIC_STRING_PATTERN.test(trimmed)) {
|
|
101
|
+
return { value, changed: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsed = Number(trimmed);
|
|
105
|
+
if (!Number.isFinite(parsed)) {
|
|
106
|
+
return { value, changed: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!matchesExpectedType(parsed, expectedTypes)) {
|
|
110
|
+
return { value, changed: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { value: parsed, changed: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tryCoerceBoolean(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
|
|
117
|
+
if (!expectedTypes.includes("boolean")) {
|
|
118
|
+
return { value, changed: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof value === "number") {
|
|
122
|
+
if (value === 0) return { value: false, changed: true };
|
|
123
|
+
if (value === 1) return { value: true, changed: true };
|
|
124
|
+
return { value, changed: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof value !== "string") {
|
|
128
|
+
return { value, changed: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
switch (value.trim().toLowerCase()) {
|
|
132
|
+
case "true":
|
|
133
|
+
case "1":
|
|
134
|
+
case "yes":
|
|
135
|
+
case "on":
|
|
136
|
+
return { value: true, changed: true };
|
|
137
|
+
case "false":
|
|
138
|
+
case "0":
|
|
139
|
+
case "no":
|
|
140
|
+
case "off":
|
|
141
|
+
return { value: false, changed: true };
|
|
142
|
+
default:
|
|
143
|
+
return { value, changed: false };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function tryCoerceBooleanToNumber(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
|
|
148
|
+
if (!expectedTypes.includes("number") && !expectedTypes.includes("integer")) {
|
|
149
|
+
return { value, changed: false };
|
|
150
|
+
}
|
|
151
|
+
if (typeof value !== "boolean") {
|
|
152
|
+
return { value, changed: false };
|
|
153
|
+
}
|
|
154
|
+
return { value: value ? 1 : 0, changed: true };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function tryCoerceString(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
|
|
158
|
+
if (!expectedTypes.includes("string") || typeof value === "string" || value === null || value === undefined) {
|
|
159
|
+
return { value, changed: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(value) || typeof value === "object") {
|
|
163
|
+
try {
|
|
164
|
+
const stringified = JSON.stringify(value);
|
|
165
|
+
if (stringified === undefined) return { value, changed: false };
|
|
166
|
+
return { value: stringified, changed: true };
|
|
167
|
+
} catch {
|
|
168
|
+
return { value, changed: false };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof value === "function") {
|
|
173
|
+
return { value, changed: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { value: String(value), changed: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function tryCoerceForExpectedTypes(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
|
|
180
|
+
if (typeof value === "string") {
|
|
181
|
+
const parsed = tryParseJsonForTypes(value, expectedTypes);
|
|
182
|
+
if (parsed.changed) return parsed;
|
|
183
|
+
return tryCoerceBoolean(value, expectedTypes);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const booleanCoercion = tryCoerceBoolean(value, expectedTypes);
|
|
187
|
+
if (booleanCoercion.changed) return booleanCoercion;
|
|
188
|
+
|
|
189
|
+
const numericCoercion = tryCoerceBooleanToNumber(value, expectedTypes);
|
|
190
|
+
if (numericCoercion.changed) return numericCoercion;
|
|
191
|
+
|
|
192
|
+
return tryCoerceString(value, expectedTypes);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function tryParseLeadingJsonContainer(value: string): unknown | undefined {
|
|
196
|
+
const firstChar = value[0];
|
|
197
|
+
const closingChar = firstChar === "{" ? "}" : firstChar === "[" ? "]" : undefined;
|
|
198
|
+
if (!closingChar) return undefined;
|
|
199
|
+
|
|
200
|
+
let depth = 0;
|
|
201
|
+
let inString = false;
|
|
202
|
+
let escaped = false;
|
|
203
|
+
|
|
204
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
205
|
+
const char = value[index];
|
|
206
|
+
|
|
207
|
+
if (inString) {
|
|
208
|
+
if (escaped) {
|
|
209
|
+
escaped = false;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === "\\") {
|
|
213
|
+
escaped = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (char === '"') inString = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (char === '"') {
|
|
221
|
+
inString = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (char === firstChar) {
|
|
226
|
+
depth += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (char !== closingChar) continue;
|
|
231
|
+
depth -= 1;
|
|
232
|
+
if (depth !== 0) continue;
|
|
233
|
+
|
|
234
|
+
const prefix = value.slice(0, index + 1);
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(prefix) as unknown;
|
|
237
|
+
} catch {
|
|
238
|
+
// LLMs sometimes emit literal `\n` or `\t` between JSON tokens
|
|
239
|
+
// (e.g. `[{...}\n]`). Convert these to real whitespace and retry.
|
|
240
|
+
const cleaned = cleanLiteralEscapes(prefix);
|
|
241
|
+
if (cleaned !== prefix) {
|
|
242
|
+
try {
|
|
243
|
+
return JSON.parse(cleaned) as unknown;
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
// Try escaping raw control chars that appear inside string literals.
|
|
247
|
+
const escapedControls = escapeRawControlsInJsonStrings(prefix);
|
|
248
|
+
if (escapedControls !== prefix) {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(escapedControls) as unknown;
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
// Also try single-char healing on the extracted prefix.
|
|
254
|
+
return tryHealMalformedJson(prefix);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Replace literal `\n`, `\t`, `\r` sequences that appear OUTSIDE of JSON
|
|
263
|
+
* strings with actual whitespace. LLMs sometimes produce these when they
|
|
264
|
+
* confuse the tool-call encoding with the content encoding.
|
|
265
|
+
*/
|
|
266
|
+
function cleanLiteralEscapes(value: string): string {
|
|
267
|
+
let result = "";
|
|
268
|
+
let inString = false;
|
|
269
|
+
let i = 0;
|
|
270
|
+
while (i < value.length) {
|
|
271
|
+
const ch = value[i];
|
|
272
|
+
if (inString) {
|
|
273
|
+
if (ch === "\\" && i + 1 < value.length) {
|
|
274
|
+
result += ch + value[i + 1];
|
|
275
|
+
i += 2;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === '"') inString = false;
|
|
279
|
+
result += ch;
|
|
280
|
+
i += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (ch === '"') {
|
|
284
|
+
inString = true;
|
|
285
|
+
result += ch;
|
|
286
|
+
i += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Outside a string: replace literal \n, \t, \r with whitespace
|
|
290
|
+
if (ch === "\\" && i + 1 < value.length) {
|
|
291
|
+
const next = value[i + 1];
|
|
292
|
+
if (next === "n" || next === "t" || next === "r") {
|
|
293
|
+
result += " ";
|
|
294
|
+
i += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
result += ch;
|
|
299
|
+
i += 1;
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Escape raw control characters (0x00–0x1F) that appear *inside* JSON string
|
|
306
|
+
* literals. LLMs sometimes emit literal newlines/tabs/etc. inside string
|
|
307
|
+
* content instead of `\n` / `\t` escape sequences, which `JSON.parse` rejects
|
|
308
|
+
* even though the surrounding structure is valid.
|
|
309
|
+
*
|
|
310
|
+
* This function only rewrites characters while inside a string; structural
|
|
311
|
+
* whitespace outside of strings is preserved unchanged.
|
|
312
|
+
*/
|
|
313
|
+
function escapeRawControlsInJsonStrings(value: string): string {
|
|
314
|
+
let result = "";
|
|
315
|
+
let inString = false;
|
|
316
|
+
let escaped = false;
|
|
317
|
+
let changed = false;
|
|
318
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
319
|
+
const ch = value[i];
|
|
320
|
+
if (inString) {
|
|
321
|
+
if (escaped) {
|
|
322
|
+
result += ch;
|
|
323
|
+
escaped = false;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (ch === "\\") {
|
|
327
|
+
result += ch;
|
|
328
|
+
escaped = true;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (ch === '"') {
|
|
332
|
+
result += ch;
|
|
333
|
+
inString = false;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const code = ch.charCodeAt(0);
|
|
337
|
+
if (code < 0x20) {
|
|
338
|
+
changed = true;
|
|
339
|
+
switch (ch) {
|
|
340
|
+
case "\n":
|
|
341
|
+
result += "\\n";
|
|
342
|
+
break;
|
|
343
|
+
case "\r":
|
|
344
|
+
result += "\\r";
|
|
345
|
+
break;
|
|
346
|
+
case "\t":
|
|
347
|
+
result += "\\t";
|
|
348
|
+
break;
|
|
349
|
+
case "\b":
|
|
350
|
+
result += "\\b";
|
|
351
|
+
break;
|
|
352
|
+
case "\f":
|
|
353
|
+
result += "\\f";
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
result += ch;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (ch === '"') {
|
|
364
|
+
inString = true;
|
|
365
|
+
}
|
|
366
|
+
result += ch;
|
|
367
|
+
}
|
|
368
|
+
return changed ? result : value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Maximum single-character edits to attempt when healing malformed JSON. */
|
|
372
|
+
const MAX_HEAL_DISTANCE = 3;
|
|
373
|
+
const BRACKET_CHARS = ["[", "]", "{", "}"] as const;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Attempts to heal near-valid JSON by applying single-character edits near the
|
|
377
|
+
* end of the string. LLMs (especially smaller ones) sometimes produce JSON with
|
|
378
|
+
* a single misplaced, extra, or wrong bracket at the end — e.g. `"}]"` becomes
|
|
379
|
+
* `"]}"` or gets an extra `}` appended. This function tries:
|
|
380
|
+
* 1. Removing a single character from the last few positions
|
|
381
|
+
* 2. Replacing a single character in the last few positions with each bracket type
|
|
382
|
+
*
|
|
383
|
+
* Returns the parsed value on success, undefined on failure.
|
|
384
|
+
*/
|
|
385
|
+
function tryHealMalformedJson(value: string): unknown | undefined {
|
|
386
|
+
// Verify it actually fails to parse
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(value) as unknown;
|
|
389
|
+
} catch {}
|
|
390
|
+
|
|
391
|
+
// Only attempt edits within the last few characters — the error is always
|
|
392
|
+
// a bracket issue at the tail for the class of LLM mistakes this targets.
|
|
393
|
+
const tailStart = Math.max(0, value.length - (MAX_HEAL_DISTANCE * 2 + 1));
|
|
394
|
+
|
|
395
|
+
// Strategy 1: remove a single character from the tail
|
|
396
|
+
for (let i = tailStart; i < value.length; i += 1) {
|
|
397
|
+
const candidate = value.slice(0, i) + value.slice(i + 1);
|
|
398
|
+
try {
|
|
399
|
+
return JSON.parse(candidate) as unknown;
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Strategy 2: replace a single character in the tail with each bracket type
|
|
404
|
+
for (let i = tailStart; i < value.length; i += 1) {
|
|
405
|
+
const original = value[i];
|
|
406
|
+
for (const replacement of BRACKET_CHARS) {
|
|
407
|
+
if (replacement === original) continue;
|
|
408
|
+
const candidate = value.slice(0, i) + replacement + value.slice(i + 1);
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(candidate) as unknown;
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const MAX_NESTED_JSON_STRING_PARSE_DEPTH = 3;
|
|
419
|
+
|
|
420
|
+
function acceptParsedJsonForTypes(
|
|
421
|
+
parsed: unknown,
|
|
422
|
+
source: string,
|
|
423
|
+
expectedTypes: string[],
|
|
424
|
+
depth: number,
|
|
425
|
+
): { value: unknown; changed: boolean } {
|
|
426
|
+
if (parsed === null && source.trim() === "null") {
|
|
427
|
+
return { value: null, changed: true };
|
|
428
|
+
}
|
|
429
|
+
if (matchesExpectedType(parsed, expectedTypes)) {
|
|
430
|
+
return { value: parsed, changed: true };
|
|
431
|
+
}
|
|
432
|
+
if (typeof parsed === "string" && !expectedTypes.includes("string") && depth < MAX_NESTED_JSON_STRING_PARSE_DEPTH) {
|
|
433
|
+
return tryParseJsonForTypes(parsed, expectedTypes, depth + 1);
|
|
434
|
+
}
|
|
435
|
+
return { value: source, changed: false };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function looksLikeJsonContainerString(value: unknown): boolean {
|
|
439
|
+
if (typeof value !== "string") return false;
|
|
440
|
+
const trimmed = value.trimStart();
|
|
441
|
+
if (trimmed.startsWith("{")) {
|
|
442
|
+
const body = trimmed.slice(1);
|
|
443
|
+
return body.trimStart().startsWith('"') || body.includes(":") || body.trimStart().startsWith("}");
|
|
444
|
+
}
|
|
445
|
+
if (!trimmed.startsWith("[")) return false;
|
|
446
|
+
const firstItem = trimmed.slice(1).trimStart();
|
|
447
|
+
return (
|
|
448
|
+
firstItem.startsWith("{") ||
|
|
449
|
+
firstItem.startsWith("[") ||
|
|
450
|
+
firstItem.startsWith('"') ||
|
|
451
|
+
firstItem.startsWith("]") ||
|
|
452
|
+
firstItem.startsWith("true") ||
|
|
453
|
+
firstItem.startsWith("false") ||
|
|
454
|
+
firstItem.startsWith("null") ||
|
|
455
|
+
/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?(?:\s*(?:,|\]|$))/.test(firstItem)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Attempts to parse a string as JSON if it looks like a JSON literal and
|
|
461
|
+
* the parsed result matches one of the expected types.
|
|
462
|
+
*
|
|
463
|
+
* Only attempts parsing for strings that syntactically look like JSON:
|
|
464
|
+
* - Objects: `{...}`
|
|
465
|
+
* - Arrays: `[...]`
|
|
466
|
+
* - Literals: `true`, `false`, `null`, or numeric strings
|
|
467
|
+
*
|
|
468
|
+
* Returns `{ changed: true }` only if parsing succeeded AND the result
|
|
469
|
+
* matches an expected type. This prevents false positives like parsing
|
|
470
|
+
* the string `"123"` when the schema actually wants a string.
|
|
471
|
+
*/
|
|
472
|
+
function tryParseJsonForTypes(value: string, expectedTypes: string[], depth = 0): { value: unknown; changed: boolean } {
|
|
473
|
+
const trimmed = value.trim();
|
|
474
|
+
if (!trimmed) return { value, changed: false };
|
|
475
|
+
|
|
476
|
+
const numberCoercion = tryParseNumberString(trimmed, expectedTypes);
|
|
477
|
+
if (numberCoercion.changed) {
|
|
478
|
+
return numberCoercion;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Quick syntactic checks to avoid unnecessary parse attempts
|
|
482
|
+
const looksJsonObject = trimmed.startsWith("{") && looksLikeJsonContainerString(trimmed);
|
|
483
|
+
const looksJsonArray = trimmed.startsWith("[") && looksLikeJsonContainerString(trimmed);
|
|
484
|
+
const looksJsonString = trimmed.startsWith('"') && !expectedTypes.includes("string");
|
|
485
|
+
const looksJsonLiteral =
|
|
486
|
+
trimmed === "true" || trimmed === "false" || trimmed === "null" || JSON_NUMBER_PATTERN.test(trimmed);
|
|
487
|
+
|
|
488
|
+
if (!looksJsonObject && !looksJsonArray && !looksJsonString && !looksJsonLiteral) {
|
|
489
|
+
return { value, changed: false };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
494
|
+
const accepted = acceptParsedJsonForTypes(parsed, trimmed, expectedTypes, depth);
|
|
495
|
+
if (accepted.changed) return accepted;
|
|
496
|
+
} catch {
|
|
497
|
+
if (looksJsonObject || looksJsonArray) {
|
|
498
|
+
// Try escaping raw control chars inside string literals (LLMs sometimes
|
|
499
|
+
// emit literal newlines/tabs inside string content rather than `\n`/`\t`).
|
|
500
|
+
const escapedControls = escapeRawControlsInJsonStrings(trimmed);
|
|
501
|
+
if (escapedControls !== trimmed) {
|
|
502
|
+
try {
|
|
503
|
+
const parsed = JSON.parse(escapedControls) as unknown;
|
|
504
|
+
const accepted = acceptParsedJsonForTypes(parsed, escapedControls, expectedTypes, depth);
|
|
505
|
+
if (accepted.changed) return accepted;
|
|
506
|
+
} catch {}
|
|
507
|
+
}
|
|
508
|
+
// Try extracting a valid JSON prefix (handles trailing junk after balanced container)
|
|
509
|
+
const leading = tryParseLeadingJsonContainer(trimmed);
|
|
510
|
+
if (leading !== undefined) {
|
|
511
|
+
const accepted = acceptParsedJsonForTypes(leading, trimmed, expectedTypes, depth);
|
|
512
|
+
if (accepted.changed) return accepted;
|
|
513
|
+
}
|
|
514
|
+
// Try healing single-character bracket errors near the end of the string
|
|
515
|
+
const healed = tryHealMalformedJson(trimmed);
|
|
516
|
+
if (healed !== undefined) {
|
|
517
|
+
const accepted = acceptParsedJsonForTypes(healed, trimmed, expectedTypes, depth);
|
|
518
|
+
if (accepted.changed) return accepted;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { value, changed: false };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { value, changed: false };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// JSON Pointer Utilities (RFC 6901)
|
|
529
|
+
// ============================================================================
|
|
530
|
+
//
|
|
531
|
+
// Internally we still address error locations using JSON Pointer syntax
|
|
532
|
+
// (e.g., `/foo/0/bar`). These utilities let coercion read and write values at
|
|
533
|
+
// those paths regardless of whether the original error came from Zod or
|
|
534
|
+
// from JSON-Schema-shaped normalization.
|
|
535
|
+
// ============================================================================
|
|
536
|
+
|
|
537
|
+
/** Encode a structured Zod issue path as a JSON Pointer. */
|
|
538
|
+
function pathToPointer(path: ReadonlyArray<PropertyKey>): string {
|
|
539
|
+
if (path.length === 0) return "";
|
|
540
|
+
return `/${path.map(seg => String(seg).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Decodes a JSON Pointer string into path segments.
|
|
545
|
+
* Handles RFC 6901 escape sequences: ~1 -> /, ~0 -> ~
|
|
546
|
+
*/
|
|
547
|
+
function decodeJsonPointer(pointer: string): string[] {
|
|
548
|
+
if (!pointer) return [];
|
|
549
|
+
return pointer
|
|
550
|
+
.split("/")
|
|
551
|
+
.slice(1) // Remove leading empty segment from initial "/"
|
|
552
|
+
.map(segment => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Retrieves a value from a nested object/array structure using a JSON Pointer.
|
|
557
|
+
* Returns undefined if the path doesn't exist or traversal fails.
|
|
558
|
+
*/
|
|
559
|
+
function getValueAtPointer(root: unknown, pointer: string): unknown {
|
|
560
|
+
if (!pointer) return root;
|
|
561
|
+
const segments = decodeJsonPointer(pointer);
|
|
562
|
+
let current: unknown = root;
|
|
563
|
+
|
|
564
|
+
for (const segment of segments) {
|
|
565
|
+
if (current === null || current === undefined) return undefined;
|
|
566
|
+
if (Array.isArray(current)) {
|
|
567
|
+
const index = Number(segment);
|
|
568
|
+
if (!Number.isInteger(index)) return undefined;
|
|
569
|
+
current = current[index];
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (typeof current !== "object") return undefined;
|
|
573
|
+
current = (current as Record<string, unknown>)[segment];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return current;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Sets a value in a nested object/array structure using a JSON Pointer.
|
|
581
|
+
* Mutates the structure in-place. Returns the root (possibly unchanged if
|
|
582
|
+
* the path was invalid).
|
|
583
|
+
*/
|
|
584
|
+
function setValueAtPointer(root: unknown, pointer: string, value: unknown): unknown {
|
|
585
|
+
if (!pointer) return value;
|
|
586
|
+
const segments = decodeJsonPointer(pointer);
|
|
587
|
+
let current: unknown = root;
|
|
588
|
+
|
|
589
|
+
// Navigate to the parent of the target location
|
|
590
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
591
|
+
const segment = segments[index];
|
|
592
|
+
if (current === null || current === undefined) return root;
|
|
593
|
+
if (Array.isArray(current)) {
|
|
594
|
+
const arrayIndex = Number(segment);
|
|
595
|
+
if (!Number.isInteger(arrayIndex)) return root;
|
|
596
|
+
current = current[arrayIndex];
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (typeof current !== "object") return root;
|
|
600
|
+
current = (current as Record<string, unknown>)[segment];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Set the value at the final segment
|
|
604
|
+
const lastSegment = segments[segments.length - 1];
|
|
605
|
+
if (Array.isArray(current)) {
|
|
606
|
+
const arrayIndex = Number(lastSegment);
|
|
607
|
+
if (!Number.isInteger(arrayIndex)) return root;
|
|
608
|
+
current[arrayIndex] = value;
|
|
609
|
+
return root;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (typeof current !== "object" || current === null) return root;
|
|
613
|
+
(current as Record<string, unknown>)[lastSegment] = value;
|
|
614
|
+
return root;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Returns a new structure with the key at `pointer` removed. Only the
|
|
619
|
+
* containers along the path are shallow-cloned (`O(depth)` allocations);
|
|
620
|
+
* every sibling subtree is shared with the input. Returns the input
|
|
621
|
+
* reference unchanged when the pointer is empty, the path is invalid, or
|
|
622
|
+
* the final key is absent — so callers can detect a no-op via identity.
|
|
623
|
+
*/
|
|
624
|
+
function deleteValueAtPointer(root: unknown, pointer: string): unknown {
|
|
625
|
+
if (!pointer) return root;
|
|
626
|
+
const segments = decodeJsonPointer(pointer);
|
|
627
|
+
if (segments.length === 0) return root;
|
|
628
|
+
return deleteAtSegment(root, segments, 0);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function deleteAtSegment(node: unknown, segments: string[], depth: number): unknown {
|
|
632
|
+
const segment = segments[depth];
|
|
633
|
+
const isLeaf = depth === segments.length - 1;
|
|
634
|
+
|
|
635
|
+
if (Array.isArray(node)) {
|
|
636
|
+
const index = Number(segment);
|
|
637
|
+
if (!Number.isInteger(index) || index < 0 || index >= node.length) return node;
|
|
638
|
+
if (isLeaf) {
|
|
639
|
+
const next = node.slice();
|
|
640
|
+
next.splice(index, 1);
|
|
641
|
+
return next;
|
|
642
|
+
}
|
|
643
|
+
const child = deleteAtSegment(node[index], segments, depth + 1);
|
|
644
|
+
if (child === node[index]) return node;
|
|
645
|
+
const next = node.slice();
|
|
646
|
+
next[index] = child;
|
|
647
|
+
return next;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (typeof node !== "object" || node === null) return node;
|
|
651
|
+
const obj = node as Record<string, unknown>;
|
|
652
|
+
if (!Object.hasOwn(obj, segment)) return node;
|
|
653
|
+
if (isLeaf) {
|
|
654
|
+
const { [segment]: _omit, ...rest } = obj;
|
|
655
|
+
return rest;
|
|
656
|
+
}
|
|
657
|
+
const child = deleteAtSegment(obj[segment], segments, depth + 1);
|
|
658
|
+
if (child === obj[segment]) return node;
|
|
659
|
+
return { ...obj, [segment]: child };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ============================================================================
|
|
663
|
+
// JSON-Schema-driven normalization passes (LLM quirks).
|
|
664
|
+
// ============================================================================
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Test a JSON-Schema branch during nullable normalization. Kept deliberately
|
|
668
|
+
* small and synchronous so validation does not need to compile legacy schemas
|
|
669
|
+
* into another schema language.
|
|
670
|
+
*/
|
|
671
|
+
function branchMatchesSchema(branch: unknown, value: unknown): boolean {
|
|
672
|
+
return isJsonSchemaValueValid(branch, value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function normalizeOptionalNullsForSchema(
|
|
676
|
+
schema: unknown,
|
|
677
|
+
value: unknown,
|
|
678
|
+
isRoot = true,
|
|
679
|
+
): { value: unknown; changed: boolean } {
|
|
680
|
+
if (value === null || value === undefined) return { value, changed: false };
|
|
681
|
+
if (schema === null || typeof schema !== "object") return { value, changed: false };
|
|
682
|
+
|
|
683
|
+
const schemaObject = schema as Record<string, unknown>;
|
|
684
|
+
|
|
685
|
+
const normalizeAnyOfLike = (keyword: "anyOf" | "oneOf"): { value: unknown; changed: boolean } => {
|
|
686
|
+
const branches = schemaObject[keyword];
|
|
687
|
+
if (!Array.isArray(branches)) return { value, changed: false };
|
|
688
|
+
|
|
689
|
+
let changedCandidate: { value: unknown; changed: true } | null = null;
|
|
690
|
+
|
|
691
|
+
for (const branch of branches) {
|
|
692
|
+
const normalized = normalizeOptionalNullsForSchema(branch, value, isRoot);
|
|
693
|
+
if (!normalized.changed) continue;
|
|
694
|
+
|
|
695
|
+
if (branchMatchesSchema(branch, normalized.value)) {
|
|
696
|
+
return normalized;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!changedCandidate) {
|
|
700
|
+
changedCandidate = { value: normalized.value, changed: true };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return changedCandidate ?? { value, changed: false };
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const anyOfNormalization = normalizeAnyOfLike("anyOf");
|
|
708
|
+
if (anyOfNormalization.changed) return anyOfNormalization;
|
|
709
|
+
|
|
710
|
+
const oneOfNormalization = normalizeAnyOfLike("oneOf");
|
|
711
|
+
if (oneOfNormalization.changed) return oneOfNormalization;
|
|
712
|
+
|
|
713
|
+
if (Array.isArray(schemaObject.allOf)) {
|
|
714
|
+
let changed = false;
|
|
715
|
+
let nextValue: unknown = value;
|
|
716
|
+
for (const branch of schemaObject.allOf) {
|
|
717
|
+
const normalized = normalizeOptionalNullsForSchema(branch, nextValue, isRoot);
|
|
718
|
+
if (!normalized.changed) continue;
|
|
719
|
+
nextValue = normalized.value;
|
|
720
|
+
changed = true;
|
|
721
|
+
}
|
|
722
|
+
if (changed) return { value: nextValue, changed: true };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (Array.isArray(value)) {
|
|
726
|
+
const itemSchema = schemaObject.items;
|
|
727
|
+
if (itemSchema === null || typeof itemSchema !== "object" || Array.isArray(itemSchema)) {
|
|
728
|
+
return { value, changed: false };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let changed = false;
|
|
732
|
+
let nextValue = value;
|
|
733
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
734
|
+
const normalized = normalizeOptionalNullsForSchema(itemSchema, value[i], false);
|
|
735
|
+
if (!normalized.changed) continue;
|
|
736
|
+
if (!changed) {
|
|
737
|
+
nextValue = [...value];
|
|
738
|
+
changed = true;
|
|
739
|
+
}
|
|
740
|
+
nextValue[i] = normalized.value;
|
|
741
|
+
}
|
|
742
|
+
return { value: changed ? nextValue : value, changed };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Coerce string → number/integer when the schema branch declares those types.
|
|
746
|
+
// This fixes anyOf:[{type:"number"},{type:"null"}] (i.e. Optional<number>) where
|
|
747
|
+
// the validator reports an "anyOf" error rather than a "type" error.
|
|
748
|
+
if ((schemaObject.type === "number" || schemaObject.type === "integer") && typeof value === "string") {
|
|
749
|
+
return tryParseNumberString(value, [schemaObject.type as string]);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (schemaObject.type !== "object") return { value, changed: false };
|
|
753
|
+
if (typeof value !== "object" || value === null) return { value, changed: false };
|
|
754
|
+
if (Array.isArray(value)) return { value, changed: false };
|
|
755
|
+
if (schemaObject.properties === null || typeof schemaObject.properties !== "object") {
|
|
756
|
+
return { value, changed: false };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const properties = schemaObject.properties as Record<string, unknown>;
|
|
760
|
+
const required = new Set(Array.isArray(schemaObject.required) ? (schemaObject.required as string[]) : []);
|
|
761
|
+
|
|
762
|
+
let changed = false;
|
|
763
|
+
let nextValue = value as Record<string, unknown>;
|
|
764
|
+
|
|
765
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
766
|
+
if (!(key in nextValue)) continue;
|
|
767
|
+
const currentValue = nextValue[key];
|
|
768
|
+
const isNullish = currentValue === null || currentValue === "null";
|
|
769
|
+
const isInvalidEmptyString =
|
|
770
|
+
currentValue === "" && !required.has(key) && !branchMatchesSchema(propertySchema, currentValue);
|
|
771
|
+
|
|
772
|
+
// Strip null/string "null" from optional fields, and strip empty
|
|
773
|
+
// strings only when the property schema would reject the explicit value.
|
|
774
|
+
// LLMs sometimes output these placeholders to mean "no value".
|
|
775
|
+
if ((isNullish || isInvalidEmptyString) && !required.has(key)) {
|
|
776
|
+
if (!changed) {
|
|
777
|
+
nextValue = { ...nextValue };
|
|
778
|
+
changed = true;
|
|
779
|
+
}
|
|
780
|
+
delete nextValue[key];
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Substitute the schema-supplied default when a required field arrives
|
|
785
|
+
// as null/"null". LLMs commonly emit null for "I have nothing to say
|
|
786
|
+
// here"; if the schema documents a default, honor it instead of
|
|
787
|
+
// rejecting the whole call. The default is cloned so mutations on the
|
|
788
|
+
// validated value never bleed back into the schema.
|
|
789
|
+
if (isNullish && propertySchema && typeof propertySchema === "object") {
|
|
790
|
+
const propertyObject = propertySchema as Record<string, unknown>;
|
|
791
|
+
if ("default" in propertyObject) {
|
|
792
|
+
if (!changed) {
|
|
793
|
+
nextValue = { ...nextValue };
|
|
794
|
+
changed = true;
|
|
795
|
+
}
|
|
796
|
+
nextValue[key] = structuredCloneJSON(propertyObject.default);
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const normalized = normalizeOptionalNullsForSchema(propertySchema, currentValue, false);
|
|
801
|
+
if (!normalized.changed) continue;
|
|
802
|
+
|
|
803
|
+
if (!changed) {
|
|
804
|
+
nextValue = { ...nextValue };
|
|
805
|
+
changed = true;
|
|
806
|
+
}
|
|
807
|
+
nextValue[key] = normalized.value;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Strip unknown keys with null/"null" values when the schema forbids extras.
|
|
811
|
+
// LLMs sometimes hallucinate verbs alongside valid ones (e.g. `split: null`,
|
|
812
|
+
// `original: null`). Rejecting the entire tool call wastes a turn; treating
|
|
813
|
+
// these the same as null on known optional fields is a safer fallback. Keys
|
|
814
|
+
// with non-null unknown values are left intact so genuine schema mistakes
|
|
815
|
+
// still surface as validation errors.
|
|
816
|
+
//
|
|
817
|
+
// At the ROOT level we deliberately keep unknown null-valued keys intact:
|
|
818
|
+
// Zod-emitted wire schemas always set `additionalProperties: false`, but the
|
|
819
|
+
// post-validation `preserveUnknownRootFields` pass re-attaches root extras
|
|
820
|
+
// so callers can observe (and reject) hallucinated fields. Stripping here
|
|
821
|
+
// would erase the field before that snapshot, hiding the rejection signal.
|
|
822
|
+
if (!isRoot && schemaObject.additionalProperties === false) {
|
|
823
|
+
const knownKeys = new Set(Object.keys(properties));
|
|
824
|
+
for (const key of Object.keys(nextValue)) {
|
|
825
|
+
if (knownKeys.has(key)) continue;
|
|
826
|
+
const v = nextValue[key];
|
|
827
|
+
if (v !== null && v !== "null") continue;
|
|
828
|
+
if (!changed) {
|
|
829
|
+
nextValue = { ...nextValue };
|
|
830
|
+
changed = true;
|
|
831
|
+
}
|
|
832
|
+
delete nextValue[key];
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return { value: changed ? nextValue : value, changed };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ============================================================================
|
|
840
|
+
// Double-encoded object-key normalization (LLM quirk).
|
|
841
|
+
// ============================================================================
|
|
842
|
+
//
|
|
843
|
+
// LLMs occasionally serialize an object key one time too many, so the property
|
|
844
|
+
// NAME arrives as the JSON encoding of the real name — literal quote characters
|
|
845
|
+
// and all (e.g. `{ "\"op\"": "done" }` decodes to the JS key `"op"`). The
|
|
846
|
+
// schema never matches such a key, so it reads as an unrecognized extra and is
|
|
847
|
+
// dropped by the unrecognized-key repair, later surfacing as a spurious
|
|
848
|
+
// missing-required error. We walk the whole value (arrays + nested objects)
|
|
849
|
+
// and rename any key that is itself the JSON encoding of a plain string back to
|
|
850
|
+
// that string.
|
|
851
|
+
// ============================================================================
|
|
852
|
+
|
|
853
|
+
/** Max layers of accidental JSON-encoding to peel off a single object key. */
|
|
854
|
+
const MAX_KEY_DECODE_DEPTH = 3;
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* If `key` is the JSON encoding of a plain string (quote-wrapped and
|
|
858
|
+
* `JSON.parse`s to a string), return the decoded string; otherwise null. Peels
|
|
859
|
+
* up to {@link MAX_KEY_DECODE_DEPTH} nested encodings so multiply-encoded keys
|
|
860
|
+
* collapse in one pass. Conservative: any key that is not a quote-wrapped JSON
|
|
861
|
+
* string literal is left untouched.
|
|
862
|
+
*/
|
|
863
|
+
function decodeDoubleEncodedKey(key: string): string | null {
|
|
864
|
+
let current = key;
|
|
865
|
+
let decoded: string | null = null;
|
|
866
|
+
for (let depth = 0; depth < MAX_KEY_DECODE_DEPTH; depth += 1) {
|
|
867
|
+
if (current.length < 2 || current[0] !== '"' || current[current.length - 1] !== '"') break;
|
|
868
|
+
let parsed: unknown;
|
|
869
|
+
try {
|
|
870
|
+
parsed = JSON.parse(current);
|
|
871
|
+
} catch {
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
if (typeof parsed !== "string") break;
|
|
875
|
+
current = parsed;
|
|
876
|
+
decoded = current;
|
|
877
|
+
}
|
|
878
|
+
return decoded;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Recursively unwrap object keys that were accidentally JSON-encoded an extra
|
|
883
|
+
* time. Schema-agnostic by design: such keys are dropped before any schema pass
|
|
884
|
+
* can map them, so this runs first. A key is only renamed when the decoded name
|
|
885
|
+
* differs and does not already exist on the same object — renaming would
|
|
886
|
+
* otherwise clobber a sibling and silently lose data.
|
|
887
|
+
*/
|
|
888
|
+
function normalizeDoubleEncodedKeys(value: unknown): { value: unknown; changed: boolean } {
|
|
889
|
+
if (Array.isArray(value)) {
|
|
890
|
+
let changed = false;
|
|
891
|
+
let next = value;
|
|
892
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
893
|
+
const normalized = normalizeDoubleEncodedKeys(value[i]);
|
|
894
|
+
if (!normalized.changed) continue;
|
|
895
|
+
if (!changed) {
|
|
896
|
+
next = [...value];
|
|
897
|
+
changed = true;
|
|
898
|
+
}
|
|
899
|
+
next[i] = normalized.value;
|
|
900
|
+
}
|
|
901
|
+
return { value: changed ? next : value, changed };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (value === null || typeof value !== "object") return { value, changed: false };
|
|
905
|
+
|
|
906
|
+
const source = value as Record<string, unknown>;
|
|
907
|
+
let changed = false;
|
|
908
|
+
const out: Record<string, unknown> = {};
|
|
909
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
910
|
+
const normalizedChild = normalizeDoubleEncodedKeys(entry);
|
|
911
|
+
const nextChild = normalizedChild.changed ? normalizedChild.value : entry;
|
|
912
|
+
|
|
913
|
+
const decodedKey = decodeDoubleEncodedKey(key);
|
|
914
|
+
// `Object.hasOwn` (not `in`) so a decoded `constructor`/`toString` is not
|
|
915
|
+
// mistaken for a collision via the prototype chain.
|
|
916
|
+
const targetKey =
|
|
917
|
+
decodedKey !== null &&
|
|
918
|
+
decodedKey !== key &&
|
|
919
|
+
!Object.hasOwn(source, decodedKey) &&
|
|
920
|
+
!Object.hasOwn(out, decodedKey)
|
|
921
|
+
? decodedKey
|
|
922
|
+
: key;
|
|
923
|
+
|
|
924
|
+
if (targetKey !== key || normalizedChild.changed) changed = true;
|
|
925
|
+
// `defineProperty` so a decoded `__proto__` key becomes an own property
|
|
926
|
+
// instead of mutating the result object's prototype.
|
|
927
|
+
Object.defineProperty(out, targetKey, {
|
|
928
|
+
value: nextChild,
|
|
929
|
+
writable: true,
|
|
930
|
+
enumerable: true,
|
|
931
|
+
configurable: true,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return { value: changed ? out : value, changed };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// ============================================================================
|
|
939
|
+
// String-encoded array coercion for union(string, array) schemas.
|
|
940
|
+
// ============================================================================
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Detects whether a schema node accepts BOTH the `string` and `array` JSON
|
|
944
|
+
* Schema types. Recognizes:
|
|
945
|
+
* - `{ "type": ["string", "array"] }` (multi-type),
|
|
946
|
+
* - `{ "anyOf": [...] }` / `{ "oneOf": [...] }` with at least one string
|
|
947
|
+
* branch and one array branch.
|
|
948
|
+
*/
|
|
949
|
+
function schemaAcceptsStringAndArray(schema: Record<string, unknown>): boolean {
|
|
950
|
+
if (Array.isArray(schema.type) && schema.type.includes("string") && schema.type.includes("array")) {
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
for (const key of ["anyOf", "oneOf"] as const) {
|
|
955
|
+
const branches = schema[key];
|
|
956
|
+
if (!Array.isArray(branches)) continue;
|
|
957
|
+
let hasString = false;
|
|
958
|
+
let hasArray = false;
|
|
959
|
+
for (const branch of branches) {
|
|
960
|
+
if (!branch || typeof branch !== "object") continue;
|
|
961
|
+
const branchType = (branch as Record<string, unknown>).type;
|
|
962
|
+
if (branchType === "string" || (Array.isArray(branchType) && branchType.includes("string"))) {
|
|
963
|
+
hasString = true;
|
|
964
|
+
}
|
|
965
|
+
if (branchType === "array" || (Array.isArray(branchType) && branchType.includes("array"))) {
|
|
966
|
+
hasArray = true;
|
|
967
|
+
}
|
|
968
|
+
if (hasString && hasArray) return true;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function schemaNodeAcceptsArray(schema: unknown): schema is Record<string, unknown> {
|
|
975
|
+
if (!schema || typeof schema !== "object") return false;
|
|
976
|
+
const schemaObject = schema as Record<string, unknown>;
|
|
977
|
+
const schemaType = schemaObject.type;
|
|
978
|
+
return schemaType === "array" || (Array.isArray(schemaType) && schemaType.includes("array"));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function parsedArrayMatchesArrayBranch(schema: Record<string, unknown>, value: unknown[]): boolean {
|
|
982
|
+
if (schemaNodeAcceptsArray(schema)) {
|
|
983
|
+
return isJsonSchemaValueValid(schema, value);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
for (const key of ["anyOf", "oneOf"] as const) {
|
|
987
|
+
const branches = schema[key];
|
|
988
|
+
if (!Array.isArray(branches)) continue;
|
|
989
|
+
const branchList: unknown[] = branches;
|
|
990
|
+
for (const branch of branchList) {
|
|
991
|
+
if (!schemaNodeAcceptsArray(branch)) continue;
|
|
992
|
+
if (isJsonSchemaValueValid(branch, value)) return true;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Pre-validation normalization: when a schema field accepts BOTH `string` and
|
|
1000
|
+
* `array`, providers that double-serialize tool arguments (e.g. Z.AI / GLM)
|
|
1001
|
+
* deliver array values as JSON-encoded strings like `'["a","b"]'`. Zod's
|
|
1002
|
+
* `union([string, array])` happily accepts that string against the string
|
|
1003
|
+
* branch, so the type-error driven coercion in {@link coerceArgsFromIssues}
|
|
1004
|
+
* never fires, and downstream tools treat the literal `["a","b"]` as a path
|
|
1005
|
+
* (silently producing zero matches or glob parse errors).
|
|
1006
|
+
*
|
|
1007
|
+
* Walk the schema; when both shapes are accepted AND the incoming value is a
|
|
1008
|
+
* JSON-array-shaped string, substitute the parsed array only if it validates
|
|
1009
|
+
* against the schema's array branch. Conservative: array-shaped strings like
|
|
1010
|
+
* `"[1]"` stay on the string branch when the array branch is `string[]`.
|
|
1011
|
+
*
|
|
1012
|
+
* See https://github.com/can1357/oh-my-pi/issues/1788.
|
|
1013
|
+
*/
|
|
1014
|
+
function normalizeStringEncodedArrayUnions(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
|
|
1015
|
+
if (value === null || value === undefined) return { value, changed: false };
|
|
1016
|
+
if (schema === null || typeof schema !== "object") return { value, changed: false };
|
|
1017
|
+
|
|
1018
|
+
const schemaObject = schema as Record<string, unknown>;
|
|
1019
|
+
|
|
1020
|
+
// Leaf case: this schema node accepts both string and array.
|
|
1021
|
+
if (typeof value === "string" && schemaAcceptsStringAndArray(schemaObject)) {
|
|
1022
|
+
const trimmed = value.trim();
|
|
1023
|
+
if (!trimmed.startsWith("[")) return { value, changed: false };
|
|
1024
|
+
try {
|
|
1025
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
1026
|
+
if (Array.isArray(parsed)) {
|
|
1027
|
+
// Unwrap any double-encoded object keys inside the parsed array
|
|
1028
|
+
// before the branch-match check; otherwise an `array<object>`
|
|
1029
|
+
// branch fails to validate and the value silently stays on the
|
|
1030
|
+
// string branch.
|
|
1031
|
+
const candidate = normalizeDoubleEncodedKeys(parsed).value as unknown[];
|
|
1032
|
+
if (parsedArrayMatchesArrayBranch(schemaObject, candidate)) {
|
|
1033
|
+
return { value: candidate, changed: true };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
// Not valid JSON — leave the string alone for the validator to handle.
|
|
1038
|
+
}
|
|
1039
|
+
return { value, changed: false };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Recurse into array items.
|
|
1043
|
+
if (Array.isArray(value)) {
|
|
1044
|
+
const itemSchema = schemaObject.items;
|
|
1045
|
+
if (!itemSchema || typeof itemSchema !== "object" || Array.isArray(itemSchema)) {
|
|
1046
|
+
return { value, changed: false };
|
|
1047
|
+
}
|
|
1048
|
+
let changed = false;
|
|
1049
|
+
let nextValue = value;
|
|
1050
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1051
|
+
const normalized = normalizeStringEncodedArrayUnions(itemSchema, value[i]);
|
|
1052
|
+
if (!normalized.changed) continue;
|
|
1053
|
+
if (!changed) {
|
|
1054
|
+
nextValue = [...value];
|
|
1055
|
+
changed = true;
|
|
1056
|
+
}
|
|
1057
|
+
nextValue[i] = normalized.value;
|
|
1058
|
+
}
|
|
1059
|
+
return { value: changed ? nextValue : value, changed };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Recurse into object properties.
|
|
1063
|
+
if (schemaObject.type !== "object") return { value, changed: false };
|
|
1064
|
+
if (typeof value !== "object" || value === null) return { value, changed: false };
|
|
1065
|
+
const properties = schemaObject.properties;
|
|
1066
|
+
if (!properties || typeof properties !== "object") return { value, changed: false };
|
|
1067
|
+
|
|
1068
|
+
const propsObject = properties as Record<string, unknown>;
|
|
1069
|
+
const valueObject = value as Record<string, unknown>;
|
|
1070
|
+
let changed = false;
|
|
1071
|
+
let nextValue = valueObject;
|
|
1072
|
+
for (const [key, propertySchema] of Object.entries(propsObject)) {
|
|
1073
|
+
if (!(key in nextValue)) continue;
|
|
1074
|
+
const normalized = normalizeStringEncodedArrayUnions(propertySchema, nextValue[key]);
|
|
1075
|
+
if (!normalized.changed) continue;
|
|
1076
|
+
if (!changed) {
|
|
1077
|
+
nextValue = { ...nextValue };
|
|
1078
|
+
changed = true;
|
|
1079
|
+
}
|
|
1080
|
+
nextValue[key] = normalized.value;
|
|
1081
|
+
}
|
|
1082
|
+
return { value: changed ? nextValue : valueObject, changed };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ============================================================================
|
|
1086
|
+
// Zod issue → coercion bridge
|
|
1087
|
+
// ============================================================================
|
|
1088
|
+
|
|
1089
|
+
interface FlatIssue {
|
|
1090
|
+
keyword: "type" | "unrecognized" | "other";
|
|
1091
|
+
instancePath: string;
|
|
1092
|
+
expectedTypes: string[];
|
|
1093
|
+
unionBranch: boolean;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Translate the Zod expected-type marker into the JSON-Schema type name our
|
|
1098
|
+
* coercion helpers already understand.
|
|
1099
|
+
*/
|
|
1100
|
+
function mapZodExpectedToJsonSchemaType(expected: unknown): string | null {
|
|
1101
|
+
if (typeof expected !== "string") return null;
|
|
1102
|
+
switch (expected) {
|
|
1103
|
+
case "string":
|
|
1104
|
+
case "number":
|
|
1105
|
+
case "boolean":
|
|
1106
|
+
case "array":
|
|
1107
|
+
case "object":
|
|
1108
|
+
case "null":
|
|
1109
|
+
return expected;
|
|
1110
|
+
case "record":
|
|
1111
|
+
return "object";
|
|
1112
|
+
case "int":
|
|
1113
|
+
case "bigint":
|
|
1114
|
+
return "integer";
|
|
1115
|
+
case "nan":
|
|
1116
|
+
return "number";
|
|
1117
|
+
default:
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Flatten Zod issues into a list of (path, expected-types) records suitable
|
|
1124
|
+
* for the coercion pass. Recurses through `invalid_union` so each inner
|
|
1125
|
+
* candidate produces independent coercion attempts.
|
|
1126
|
+
*/
|
|
1127
|
+
function flattenIssues(issues: ReadonlyArray<ZodIssue>): FlatIssue[] {
|
|
1128
|
+
const out: FlatIssue[] = [];
|
|
1129
|
+
const walk = (issue: ZodIssue, prefix: ReadonlyArray<PropertyKey>, unionBranch: boolean): void => {
|
|
1130
|
+
const fullPath = prefix.length === 0 ? issue.path : [...prefix, ...issue.path];
|
|
1131
|
+
if (issue.code === "invalid_type") {
|
|
1132
|
+
const mapped = mapZodExpectedToJsonSchemaType((issue as { expected?: unknown }).expected);
|
|
1133
|
+
if (mapped) {
|
|
1134
|
+
out.push({ keyword: "type", instancePath: pathToPointer(fullPath), expectedTypes: [mapped], unionBranch });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (issue.code === "unrecognized_keys") {
|
|
1139
|
+
const keys = (issue as { keys?: ReadonlyArray<string> }).keys ?? [];
|
|
1140
|
+
for (const key of keys) {
|
|
1141
|
+
out.push({
|
|
1142
|
+
keyword: "unrecognized",
|
|
1143
|
+
instancePath: pathToPointer([...fullPath, key]),
|
|
1144
|
+
expectedTypes: [],
|
|
1145
|
+
unionBranch,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (issue.code === "invalid_union") {
|
|
1151
|
+
const inner = (issue as unknown as { errors?: ReadonlyArray<ReadonlyArray<ZodIssue>> }).errors;
|
|
1152
|
+
if (inner) {
|
|
1153
|
+
// A union-branch issue only competes with a sibling branch when it
|
|
1154
|
+
// sits at the union node's own path. Issues whose own path is
|
|
1155
|
+
// non-empty live on a deeper field that an already-identified
|
|
1156
|
+
// branch owns, so the singleton-array repair should still apply.
|
|
1157
|
+
for (const branch of inner) {
|
|
1158
|
+
for (const child of branch) {
|
|
1159
|
+
walk(child, fullPath, child.path.length === 0);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
out.push({ keyword: "other", instancePath: pathToPointer(fullPath), expectedTypes: [], unionBranch });
|
|
1166
|
+
};
|
|
1167
|
+
for (const issue of issues) walk(issue, [], false);
|
|
1168
|
+
return out;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Repair issues raised by the validator before we surface them to the caller.
|
|
1173
|
+
*
|
|
1174
|
+
* Two kinds of repair are applied:
|
|
1175
|
+
* - **type**: when a value has a common LLM-produced shape mismatch, rewrite
|
|
1176
|
+
* it only in the direction requested by the schema: parse JSON strings,
|
|
1177
|
+
* accept boolean spellings, stringify non-null values for string fields,
|
|
1178
|
+
* map booleans to numeric 0/1, and wrap singleton array values for non-union
|
|
1179
|
+
* array expectations.
|
|
1180
|
+
* - **unrecognized**: when a strict object received an extra key (Zod's
|
|
1181
|
+
* `unrecognized_keys` or JSON Schema's `additionalProperties: false`),
|
|
1182
|
+
* drop that key so re-validation succeeds. This effectively coerces every
|
|
1183
|
+
* object schema to loose semantics recursively without rebuilding the
|
|
1184
|
+
* underlying Zod tree.
|
|
1185
|
+
*
|
|
1186
|
+
* The function is safe and conservative:
|
|
1187
|
+
* - Only processes "type" and "unrecognized" issues
|
|
1188
|
+
* - Only attempts schema-directed coercions for the expected type
|
|
1189
|
+
* - Only wraps singleton array values for non-union type expectations
|
|
1190
|
+
* - Clones the args object before mutation (copy-on-write)
|
|
1191
|
+
*/
|
|
1192
|
+
function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unknown; changed: boolean } {
|
|
1193
|
+
if (issues.length === 0) return { value: args, changed: false };
|
|
1194
|
+
|
|
1195
|
+
let changed = false;
|
|
1196
|
+
// Tracks whether `nextArgs` is a fully owned deep copy (safe to mutate
|
|
1197
|
+
// leaves). The unrecognized-key path uses path-shallow immutable updates
|
|
1198
|
+
// and does NOT require ownership, so we only pay for the deep clone when
|
|
1199
|
+
// a type coercion actually needs to write into a leaf.
|
|
1200
|
+
let owned = false;
|
|
1201
|
+
let nextArgs: unknown = args;
|
|
1202
|
+
|
|
1203
|
+
for (const issue of issues) {
|
|
1204
|
+
if (issue.keyword === "unrecognized") {
|
|
1205
|
+
const previous = nextArgs;
|
|
1206
|
+
nextArgs = deleteValueAtPointer(nextArgs, issue.instancePath);
|
|
1207
|
+
if (nextArgs !== previous) changed = true;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (issue.keyword !== "type") continue;
|
|
1211
|
+
if (issue.expectedTypes.length === 0) continue;
|
|
1212
|
+
|
|
1213
|
+
const currentValue = getValueAtPointer(nextArgs, issue.instancePath);
|
|
1214
|
+
const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes);
|
|
1215
|
+
let coercedValue = result.changed ? result.value : undefined;
|
|
1216
|
+
if (
|
|
1217
|
+
coercedValue === undefined &&
|
|
1218
|
+
issue.expectedTypes.includes("array") &&
|
|
1219
|
+
!issue.unionBranch &&
|
|
1220
|
+
currentValue !== undefined &&
|
|
1221
|
+
!Array.isArray(currentValue)
|
|
1222
|
+
) {
|
|
1223
|
+
const objectCoercion =
|
|
1224
|
+
typeof currentValue === "string"
|
|
1225
|
+
? tryParseJsonForTypes(currentValue, ["object"])
|
|
1226
|
+
: { value: currentValue, changed: false };
|
|
1227
|
+
if (objectCoercion.changed || !looksLikeJsonContainerString(currentValue)) {
|
|
1228
|
+
coercedValue = [objectCoercion.changed ? objectCoercion.value : currentValue];
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (coercedValue === undefined) continue;
|
|
1232
|
+
|
|
1233
|
+
if (!owned) {
|
|
1234
|
+
nextArgs = structuredCloneJSON(nextArgs);
|
|
1235
|
+
owned = true;
|
|
1236
|
+
changed = true;
|
|
1237
|
+
}
|
|
1238
|
+
nextArgs = setValueAtPointer(nextArgs, issue.instancePath, coercedValue);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
return { value: changed ? nextArgs : args, changed };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// ============================================================================
|
|
1245
|
+
// Public API
|
|
1246
|
+
// ============================================================================
|
|
1247
|
+
|
|
1248
|
+
type ValidationContext =
|
|
1249
|
+
| {
|
|
1250
|
+
kind: "zod";
|
|
1251
|
+
zod: ZodType;
|
|
1252
|
+
json: Record<string, unknown>;
|
|
1253
|
+
}
|
|
1254
|
+
| {
|
|
1255
|
+
kind: "arktype";
|
|
1256
|
+
ark: Type;
|
|
1257
|
+
json: Record<string, unknown>;
|
|
1258
|
+
}
|
|
1259
|
+
| {
|
|
1260
|
+
kind: "json";
|
|
1261
|
+
json: Record<string, unknown>;
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Cache the validation context derived from a tool's parameters schema.
|
|
1266
|
+
* Keyed by the parameters object identity (stable across tool registrations),
|
|
1267
|
+
* via {@link stamp} so callable ArkType schemas — and any frozen host — degrade
|
|
1268
|
+
* to recompute-on-call instead of throwing on assignment.
|
|
1269
|
+
*/
|
|
1270
|
+
const kValidationContext = Symbol("ai.validationContext");
|
|
1271
|
+
function getValidationContext(tool: Tool): ValidationContext {
|
|
1272
|
+
return stamp(tool.parameters as object, kValidationContext, params =>
|
|
1273
|
+
isArkSchema(params)
|
|
1274
|
+
? { kind: "arktype", ark: params, json: arkToWireSchema(params) }
|
|
1275
|
+
: isZodSchema(params)
|
|
1276
|
+
? { kind: "zod", zod: params, json: zodToWireSchema(params) }
|
|
1277
|
+
: { kind: "json", json: upgradeJsonSchemaTo202012(params) as Record<string, unknown> },
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
type ContextValidationResult =
|
|
1282
|
+
| { success: true; value: unknown }
|
|
1283
|
+
| { success: false; flatIssues: FlatIssue[]; messages: string[] };
|
|
1284
|
+
|
|
1285
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1286
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function preserveUnknownRootFields(input: unknown, parsed: unknown): unknown {
|
|
1290
|
+
if (!isPlainRecord(input) || !isPlainRecord(parsed)) return parsed;
|
|
1291
|
+
return { ...input, ...parsed };
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function flattenJsonSchemaIssues(issues: ReadonlyArray<JsonSchemaValidationIssue>): FlatIssue[] {
|
|
1295
|
+
return issues.map(issue => {
|
|
1296
|
+
const unionBranch = issue.fromUnionBranch === true;
|
|
1297
|
+
if (issue.keyword === "additionalProperties") {
|
|
1298
|
+
return {
|
|
1299
|
+
keyword: "unrecognized",
|
|
1300
|
+
instancePath: pathToPointer(issue.path),
|
|
1301
|
+
expectedTypes: [],
|
|
1302
|
+
unionBranch,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
keyword: issue.keyword === "type" ? "type" : "other",
|
|
1307
|
+
instancePath: pathToPointer(issue.path),
|
|
1308
|
+
expectedTypes: issue.expectedTypes ?? [],
|
|
1309
|
+
unionBranch,
|
|
1310
|
+
};
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
1315
|
+
return path.length === 0 ? "root" : path.map(seg => String(seg)).join("/");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function validateContext(ctx: ValidationContext, value: unknown): ContextValidationResult {
|
|
1319
|
+
if (ctx.kind === "zod") {
|
|
1320
|
+
const result = ctx.zod.safeParse(value);
|
|
1321
|
+
if (result.success) {
|
|
1322
|
+
return { success: true, value: preserveUnknownRootFields(value, result.data) };
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
success: false,
|
|
1326
|
+
flatIssues: flattenIssues(result.error.issues),
|
|
1327
|
+
messages: result.error.issues.map(issue => ` - ${formatIssuePath(issue.path)}: ${issue.message}`),
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (ctx.kind === "arktype") {
|
|
1332
|
+
const out = ctx.ark(value);
|
|
1333
|
+
if (!(out instanceof type.errors)) {
|
|
1334
|
+
return { success: true, value: preserveUnknownRootFields(value, out) };
|
|
1335
|
+
}
|
|
1336
|
+
// A `.narrow()`/cross-field failure can have ArkType reject while the wire
|
|
1337
|
+
// JSON (its predicate dropped by the toJsonSchema fallback) accepts — then
|
|
1338
|
+
// there are no json issues to coerce and we fall through to the formatted
|
|
1339
|
+
// error built from ArkType's own messages.
|
|
1340
|
+
const jr = validateJsonSchemaValue(ctx.json, value);
|
|
1341
|
+
const flatIssues = jr.success ? [] : flattenJsonSchemaIssues(jr.issues);
|
|
1342
|
+
return {
|
|
1343
|
+
success: false,
|
|
1344
|
+
flatIssues,
|
|
1345
|
+
messages: out.map(e => ` - ${formatIssuePath(e.path)}: ${e.message}`),
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const result = validateJsonSchemaValue(ctx.json, value);
|
|
1350
|
+
if (result.success) return { success: true, value };
|
|
1351
|
+
return {
|
|
1352
|
+
success: false,
|
|
1353
|
+
flatIssues: flattenJsonSchemaIssues(result.issues),
|
|
1354
|
+
messages: result.issues.map(issue => ` - ${formatIssuePath(issue.path)}: ${issue.message}`),
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const MAX_COERCION_PASSES = 5;
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Finds a tool by name and validates the tool call arguments against its schema.
|
|
1362
|
+
* @param tools Array of tool definitions
|
|
1363
|
+
* @param toolCall The tool call from the LLM
|
|
1364
|
+
* @returns The validated arguments
|
|
1365
|
+
* @throws Error if tool is not found or validation fails
|
|
1366
|
+
*/
|
|
1367
|
+
export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"] {
|
|
1368
|
+
const tool = tools.find(t => t.name === toolCall.name);
|
|
1369
|
+
if (!tool) {
|
|
1370
|
+
throw new AIError.ToolNotFoundError(toolCall.name);
|
|
1371
|
+
}
|
|
1372
|
+
return validateToolArguments(tool, toolCall);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Cap per-field string lengths when embedding received args in an error message. */
|
|
1376
|
+
const MAX_ERROR_ARG_STRING_LENGTH = 256;
|
|
1377
|
+
|
|
1378
|
+
function truncateArgsForError(value: unknown): unknown {
|
|
1379
|
+
if (typeof value === "string") {
|
|
1380
|
+
if (value.length <= MAX_ERROR_ARG_STRING_LENGTH) return value;
|
|
1381
|
+
return `${value.slice(0, MAX_ERROR_ARG_STRING_LENGTH)}… [truncated ${value.length - MAX_ERROR_ARG_STRING_LENGTH} chars]`;
|
|
1382
|
+
}
|
|
1383
|
+
if (Array.isArray(value)) return value.map(truncateArgsForError);
|
|
1384
|
+
if (value !== null && typeof value === "object") {
|
|
1385
|
+
const out: Record<string, unknown> = {};
|
|
1386
|
+
for (const [key, entry] of Object.entries(value)) out[key] = truncateArgsForError(entry);
|
|
1387
|
+
return out;
|
|
1388
|
+
}
|
|
1389
|
+
return value;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
1394
|
+
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
1395
|
+
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
1396
|
+
* declaring failure.
|
|
1397
|
+
*
|
|
1398
|
+
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
1399
|
+
*/
|
|
1400
|
+
export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"] {
|
|
1401
|
+
const originalArgs = toolCall.arguments;
|
|
1402
|
+
if (originalArgs && typeof originalArgs === "object" && "__parseError" in originalArgs) {
|
|
1403
|
+
const parseError = originalArgs.__parseError;
|
|
1404
|
+
const rawJson = String(originalArgs.__rawJson ?? "");
|
|
1405
|
+
const maxLen = 512;
|
|
1406
|
+
const truncatedRawJson =
|
|
1407
|
+
rawJson.length <= maxLen
|
|
1408
|
+
? rawJson
|
|
1409
|
+
: `${rawJson.slice(0, maxLen)}… [truncated ${rawJson.length - maxLen} chars]`;
|
|
1410
|
+
throw new AIError.ValidationError(
|
|
1411
|
+
`Validation failed for tool "${toolCall.name}": Tool call arguments are not valid JSON.\nParse Error: ${parseError}\nRaw JSON:\n${truncatedRawJson}`,
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
const ctx = getValidationContext(tool);
|
|
1415
|
+
const { json } = ctx;
|
|
1416
|
+
|
|
1417
|
+
// Always normalize first — strip null/string "null" from optional fields,
|
|
1418
|
+
// strip optional empty strings only when their property schema rejects the
|
|
1419
|
+
// explicit value, and substitute defaults. Handles LLM outputting
|
|
1420
|
+
// placeholders for "no value" even when validation would otherwise pass.
|
|
1421
|
+
let normalizedArgs: unknown = originalArgs;
|
|
1422
|
+
let changed = false;
|
|
1423
|
+
|
|
1424
|
+
// Unwrap accidentally double-JSON-encoded object keys before any schema
|
|
1425
|
+
// pass. LLMs sometimes emit `{ "\"op\"": "done" }`, so the property name
|
|
1426
|
+
// arrives quote-wrapped; left alone it reads as an unrecognized key, gets
|
|
1427
|
+
// dropped by the coercion repair, and re-surfaces as a missing-required
|
|
1428
|
+
// error. Running first means every later pass sees the corrected names.
|
|
1429
|
+
const keyNormalization = normalizeDoubleEncodedKeys(normalizedArgs);
|
|
1430
|
+
if (keyNormalization.changed) {
|
|
1431
|
+
normalizedArgs = keyNormalization.value;
|
|
1432
|
+
changed = true;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|
|
1436
|
+
if (initialNormalization.changed) {
|
|
1437
|
+
normalizedArgs = initialNormalization.value;
|
|
1438
|
+
changed = true;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Then re-shape JSON-stringified arrays whose schema accepts both string
|
|
1442
|
+
// and array (e.g. `paths: string | string[]`). Without this, zod accepts
|
|
1443
|
+
// the literal `'["a","b"]'` as a string and downstream tools treat it as
|
|
1444
|
+
// a single path with embedded glob brackets — silent zero results.
|
|
1445
|
+
const stringEncodedArrayNorm = normalizeStringEncodedArrayUnions(json, normalizedArgs);
|
|
1446
|
+
if (stringEncodedArrayNorm.changed) {
|
|
1447
|
+
normalizedArgs = stringEncodedArrayNorm.value;
|
|
1448
|
+
changed = true;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
let result = validateContext(ctx, normalizedArgs);
|
|
1452
|
+
if (result.success) return result.value as ToolCall["arguments"];
|
|
1453
|
+
|
|
1454
|
+
for (let pass = 0; pass < MAX_COERCION_PASSES; pass += 1) {
|
|
1455
|
+
const coercion = coerceArgsFromIssues(normalizedArgs, result.flatIssues);
|
|
1456
|
+
if (!coercion.changed) break;
|
|
1457
|
+
|
|
1458
|
+
normalizedArgs = coercion.value;
|
|
1459
|
+
changed = true;
|
|
1460
|
+
|
|
1461
|
+
// `coerceArgsFromIssues` may have just parsed a JSON-string container at
|
|
1462
|
+
// the root or a nested field, exposing double-encoded keys the initial
|
|
1463
|
+
// pass could not reach. Re-unwrap before the unrecognized-key repair on
|
|
1464
|
+
// the next validation pass would delete them.
|
|
1465
|
+
const keyNormalizationPass = normalizeDoubleEncodedKeys(normalizedArgs);
|
|
1466
|
+
if (keyNormalizationPass.changed) {
|
|
1467
|
+
normalizedArgs = keyNormalizationPass.value;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const nullNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|
|
1471
|
+
if (nullNormalization.changed) {
|
|
1472
|
+
normalizedArgs = nullNormalization.value;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Re-run the union-string coercion because `coerceArgsFromIssues` may
|
|
1476
|
+
// have just unwrapped a JSON-stringified object at the root or inside a
|
|
1477
|
+
// nested field — exposing `string | string[]` descendants the initial
|
|
1478
|
+
// pre-validation pass could not reach.
|
|
1479
|
+
const stringEncodedArrayNormPass = normalizeStringEncodedArrayUnions(json, normalizedArgs);
|
|
1480
|
+
if (stringEncodedArrayNormPass.changed) {
|
|
1481
|
+
normalizedArgs = stringEncodedArrayNormPass.value;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
result = validateContext(ctx, normalizedArgs);
|
|
1485
|
+
if (result.success) return result.value as ToolCall["arguments"];
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Format validation errors nicely. The header phrase is asserted by
|
|
1489
|
+
// existing tests; the detailed body is informational.
|
|
1490
|
+
const errors = result.messages.join("\n") || "Unknown validation error";
|
|
1491
|
+
|
|
1492
|
+
// Truncate long per-field strings: the full payload (potentially hundreds
|
|
1493
|
+
// of KB for write/edit-class calls) would otherwise round-trip back to the
|
|
1494
|
+
// model inside the tool error.
|
|
1495
|
+
const receivedArgs = changed
|
|
1496
|
+
? {
|
|
1497
|
+
original: truncateArgsForError(originalArgs),
|
|
1498
|
+
normalized: truncateArgsForError(normalizedArgs),
|
|
1499
|
+
}
|
|
1500
|
+
: truncateArgsForError(originalArgs);
|
|
1501
|
+
|
|
1502
|
+
const errorMessage = `Validation failed for tool "${
|
|
1503
|
+
toolCall.name
|
|
1504
|
+
}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;
|
|
1505
|
+
|
|
1506
|
+
throw new AIError.ValidationError(errorMessage);
|
|
1507
|
+
}
|