@iislee/opencodex 2.11.0 → 2.35.0
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/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import type { AdapterRequest, ProviderAdapter } from "./base";
|
|
2
2
|
import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
|
|
3
|
-
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName,
|
|
3
|
+
import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
4
4
|
import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
5
5
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
6
|
+
import { sseFieldValue } from "../lib/sse-decoder";
|
|
6
7
|
import { isDebugEnabled } from "../lib/debug-settings";
|
|
7
8
|
import { isCyberPolicyCode } from "../lib/errors";
|
|
8
9
|
import { redactSecretString } from "../lib/redact";
|
|
9
10
|
import { contentPartsToText } from "./image";
|
|
10
|
-
import {
|
|
11
|
+
import { identifyRoutedModel } from "./identity";
|
|
12
|
+
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
11
13
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
12
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
|
+
import {
|
|
16
|
+
canForwardForeignServiceTierForChatModel,
|
|
17
|
+
fastPolicyForModel,
|
|
18
|
+
supportsServiceTierForModel,
|
|
19
|
+
} from "../providers/service-tier";
|
|
20
|
+
import {
|
|
21
|
+
canonicalFastTierMarker,
|
|
22
|
+
createAdapterTierMetadata,
|
|
23
|
+
decideTier,
|
|
24
|
+
type AdapterTierMetadata,
|
|
25
|
+
type ResolvedFastPolicy,
|
|
26
|
+
} from "../providers/fastwire";
|
|
27
|
+
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
28
|
+
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
29
|
+
import {
|
|
30
|
+
isXaiSchemaTarget,
|
|
31
|
+
lookupLocalJsonPointer,
|
|
32
|
+
normalizeXaiToolParameters,
|
|
33
|
+
} from "./xai-tool-schema";
|
|
13
34
|
import {
|
|
14
35
|
isTranslatorBudgetExceededError,
|
|
15
36
|
retainTranslatedEventBatch,
|
|
@@ -21,7 +42,148 @@ import {
|
|
|
21
42
|
// Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
|
|
22
43
|
// unflagged OpenAI-compatible providers and the Anthropic adapter keep ids verbatim.
|
|
23
44
|
export function stripBracketedModelSuffix(modelId: string): string {
|
|
24
|
-
|
|
45
|
+
const suffixEnd = modelId.trimEnd().length;
|
|
46
|
+
if (suffixEnd === 0 || modelId[suffixEnd - 1] !== "]") return modelId;
|
|
47
|
+
|
|
48
|
+
let suffixStart = -1;
|
|
49
|
+
for (let i = suffixEnd - 2; i >= 0 && modelId[i] !== "]"; i--) {
|
|
50
|
+
if (modelId[i] === "[") suffixStart = i;
|
|
51
|
+
}
|
|
52
|
+
return suffixStart === -1 ? modelId : modelId.slice(0, suffixStart);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CHAT_PASSTHROUGH_FIELDS = [
|
|
56
|
+
"audio",
|
|
57
|
+
"frequency_penalty",
|
|
58
|
+
"logit_bias",
|
|
59
|
+
"logprobs",
|
|
60
|
+
"max_completion_tokens",
|
|
61
|
+
"max_tokens",
|
|
62
|
+
"metadata",
|
|
63
|
+
"modalities",
|
|
64
|
+
"n",
|
|
65
|
+
"prediction",
|
|
66
|
+
"presence_penalty",
|
|
67
|
+
"reasoning_effort",
|
|
68
|
+
"response_format",
|
|
69
|
+
"seed",
|
|
70
|
+
"stop",
|
|
71
|
+
"store",
|
|
72
|
+
"temperature",
|
|
73
|
+
"tool_choice",
|
|
74
|
+
"tools",
|
|
75
|
+
"top_logprobs",
|
|
76
|
+
"top_p",
|
|
77
|
+
"user",
|
|
78
|
+
"web_search_options",
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
function openAIChatTransport(provider: OcxProviderConfig): {
|
|
82
|
+
url: string;
|
|
83
|
+
headers: Record<string, string>;
|
|
84
|
+
hasCredential: boolean;
|
|
85
|
+
} {
|
|
86
|
+
const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
|
|
87
|
+
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
88
|
+
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
89
|
+
}
|
|
90
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
91
|
+
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
92
|
+
if (provider.headers) Object.assign(headers, provider.headers);
|
|
93
|
+
return { url: openaiChatCompletionsUrl(provider.baseUrl), headers, hasCredential };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a provider request from an inbound Chat Completions body without translating it
|
|
98
|
+
* through the Responses contract. This is deliberately a whitelist: Chat-only caller
|
|
99
|
+
* fields retain their exact wire representation, while provider capability gates remain
|
|
100
|
+
* centralized beside the ordinary openai-chat adapter.
|
|
101
|
+
*/
|
|
102
|
+
export function buildOpenAIChatPassthroughRequest(
|
|
103
|
+
provider: OcxProviderConfig,
|
|
104
|
+
rawBody: Record<string, unknown>,
|
|
105
|
+
modelId: string,
|
|
106
|
+
stream: boolean,
|
|
107
|
+
fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
|
|
108
|
+
fastMode?: boolean,
|
|
109
|
+
): AdapterRequest {
|
|
110
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
111
|
+
|
|
112
|
+
const body: Record<string, unknown> = {
|
|
113
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
114
|
+
messages: rawBody.messages,
|
|
115
|
+
stream,
|
|
116
|
+
};
|
|
117
|
+
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
118
|
+
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
122
|
+
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
123
|
+
|
|
124
|
+
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
125
|
+
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
126
|
+
if (modelInList(provider.noPenaltyModels, modelId)) {
|
|
127
|
+
delete body.presence_penalty;
|
|
128
|
+
delete body.frequency_penalty;
|
|
129
|
+
}
|
|
130
|
+
// Exact match, unlike the gates above: `noStructuredOutputModels` is documented as
|
|
131
|
+
// "only an exact requested-model match omits the field" (#1424), and the Responses
|
|
132
|
+
// ingress enforces exactly that. A prefix match here would strip response_format from
|
|
133
|
+
// `<listed>:<tag>` siblings the operator never opted out, silently returning prose.
|
|
134
|
+
if (provider.noStructuredOutputModels?.includes(modelId)) delete body.response_format;
|
|
135
|
+
|
|
136
|
+
// Run the same complete Fast policy as the translated Chat path, including explicit
|
|
137
|
+
// fastMode and foreign-tier handling. On inherited canonical Fast, the passthrough still
|
|
138
|
+
// retains the caller's exact spelling; forced Fast uses the policy-owned wire value.
|
|
139
|
+
const callerTier = typeof rawBody.service_tier === "string" ? rawBody.service_tier : undefined;
|
|
140
|
+
const tierDecision = decideTier(fastPolicy, fastMode, callerTier);
|
|
141
|
+
if (tierDecision.kind === "set") {
|
|
142
|
+
body.service_tier = fastMode === undefined && canonicalFastTierMarker(callerTier) !== undefined
|
|
143
|
+
? callerTier
|
|
144
|
+
: tierDecision.value;
|
|
145
|
+
} else if (tierDecision.kind === "forward-caller" && rawBody.service_tier !== undefined) {
|
|
146
|
+
body.service_tier = rawBody.service_tier;
|
|
147
|
+
}
|
|
148
|
+
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
149
|
+
body.prompt_cache_key = rawBody.prompt_cache_key;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
|
|
152
|
+
if (provider.parallelToolCalls === true) {
|
|
153
|
+
body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
|
|
154
|
+
} else if (provider.parallelToolCalls === false
|
|
155
|
+
&& (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
|
|
156
|
+
body.parallel_tool_calls = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (stream) {
|
|
160
|
+
const callerOptions = rawBody.stream_options !== null
|
|
161
|
+
&& typeof rawBody.stream_options === "object"
|
|
162
|
+
&& !Array.isArray(rawBody.stream_options)
|
|
163
|
+
? rawBody.stream_options as Record<string, unknown>
|
|
164
|
+
: {};
|
|
165
|
+
body.stream_options = { ...callerOptions, include_usage: true };
|
|
166
|
+
} else if (rawBody.stream_options !== undefined) {
|
|
167
|
+
body.stream_options = rawBody.stream_options;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const bodyJson = JSON.stringify(body);
|
|
171
|
+
|
|
172
|
+
if (isDebugEnabled()) {
|
|
173
|
+
let host = "upstream";
|
|
174
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
175
|
+
debugProviderDiagnostic("openai-chat", "passthrough-request", {
|
|
176
|
+
host,
|
|
177
|
+
model: body.model,
|
|
178
|
+
stream,
|
|
179
|
+
messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
|
|
180
|
+
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
181
|
+
hasCredential,
|
|
182
|
+
bodyBytes: new TextEncoder().encode(bodyJson).length,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { url, method: "POST", headers, body: bodyJson };
|
|
25
187
|
}
|
|
26
188
|
|
|
27
189
|
// 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
|
|
@@ -46,14 +208,12 @@ function extractErrorDetail(parsed: unknown): string | undefined {
|
|
|
46
208
|
if (typeof parsed === "string") return parsed.trim() || undefined;
|
|
47
209
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
48
210
|
const obj = parsed as Record<string, unknown>;
|
|
49
|
-
// OpenAI shape: { error: { message } } or { error: "..." }
|
|
50
211
|
const err = obj.error;
|
|
51
212
|
if (typeof err === "string" && err.trim()) return err.trim();
|
|
52
213
|
if (err !== null && typeof err === "object" && !Array.isArray(err)) {
|
|
53
214
|
const msg = (err as Record<string, unknown>).message;
|
|
54
215
|
if (typeof msg === "string" && msg.trim()) return msg.trim();
|
|
55
216
|
}
|
|
56
|
-
// FastAPI/pydantic shape (NVIDIA NIM): { detail: "..." } or { detail: [{ msg, loc }, ...] }
|
|
57
217
|
const det = obj.detail;
|
|
58
218
|
if (typeof det === "string" && det.trim()) return det.trim();
|
|
59
219
|
if (Array.isArray(det)) {
|
|
@@ -64,12 +224,349 @@ function extractErrorDetail(parsed: unknown): string | undefined {
|
|
|
64
224
|
.filter(m => m.length > 0);
|
|
65
225
|
if (msgs.length > 0) return msgs.join("; ");
|
|
66
226
|
}
|
|
67
|
-
// Generic fallbacks: { message } / RFC7807 { title }
|
|
68
227
|
if (typeof obj.message === "string" && obj.message.trim()) return obj.message.trim();
|
|
69
228
|
if (typeof obj.title === "string" && obj.title.trim()) return obj.title.trim();
|
|
70
229
|
return undefined;
|
|
71
230
|
}
|
|
72
231
|
|
|
232
|
+
function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
|
|
233
|
+
if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
|
|
234
|
+
const data = json.data;
|
|
235
|
+
return data !== null && typeof data === "object" && !Array.isArray(data)
|
|
236
|
+
? data as Record<string, unknown>
|
|
237
|
+
: json;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface OpenAIChatError {
|
|
241
|
+
message?: unknown;
|
|
242
|
+
code?: unknown;
|
|
243
|
+
type?: unknown;
|
|
244
|
+
status?: unknown;
|
|
245
|
+
metadata?: unknown;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function safeUpstreamRequestId(metadata: unknown): string | undefined {
|
|
249
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
250
|
+
const record = metadata as Record<string, unknown>;
|
|
251
|
+
const value = record.request_id ?? record.requestId;
|
|
252
|
+
if (typeof value !== "string") return undefined;
|
|
253
|
+
const requestId = value.trim();
|
|
254
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
|
|
255
|
+
&& redactSecretString(requestId) === requestId
|
|
256
|
+
? requestId
|
|
257
|
+
: undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function upstreamErrorEvent(
|
|
261
|
+
error: unknown,
|
|
262
|
+
usage?: OcxUsage,
|
|
263
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
264
|
+
const details = error !== null && typeof error === "object" && !Array.isArray(error)
|
|
265
|
+
? error as OpenAIChatError
|
|
266
|
+
: undefined;
|
|
267
|
+
const rawMessage = typeof error === "string"
|
|
268
|
+
? error.trim() || "upstream error"
|
|
269
|
+
: typeof details?.message === "string" ? details.message : "upstream error";
|
|
270
|
+
const safeMessage = redactSecretString(rawMessage);
|
|
271
|
+
const requestId = safeUpstreamRequestId(details?.metadata);
|
|
272
|
+
const message = requestId !== undefined && !safeMessage.includes(requestId)
|
|
273
|
+
? `${safeMessage} (request ID: ${requestId})`
|
|
274
|
+
: safeMessage;
|
|
275
|
+
const code = typeof details?.code === "string"
|
|
276
|
+
? details.code
|
|
277
|
+
: typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
|
|
278
|
+
? String(details.code)
|
|
279
|
+
: undefined;
|
|
280
|
+
const errorType = typeof details?.type === "string" ? details.type : undefined;
|
|
281
|
+
const codeStatus = typeof details?.code === "number"
|
|
282
|
+
&& Number.isInteger(details.code)
|
|
283
|
+
&& details.code >= 100
|
|
284
|
+
&& details.code <= 599
|
|
285
|
+
? details.code
|
|
286
|
+
: undefined;
|
|
287
|
+
const status = isCyberPolicyCode(code)
|
|
288
|
+
? 400
|
|
289
|
+
: typeof details?.status === "number" && Number.isInteger(details.status)
|
|
290
|
+
? details.status
|
|
291
|
+
: codeStatus;
|
|
292
|
+
return {
|
|
293
|
+
type: "error",
|
|
294
|
+
message,
|
|
295
|
+
...(usage !== undefined ? { usage } : {}),
|
|
296
|
+
...(code !== undefined ? { code } : {}),
|
|
297
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
298
|
+
...(status !== undefined ? { status } : {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
|
|
303
|
+
return finishReason === "length"
|
|
304
|
+
? "max_tokens"
|
|
305
|
+
: finishReason === "content_filter"
|
|
306
|
+
? "content_filter"
|
|
307
|
+
: undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
|
|
311
|
+
return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
|
|
312
|
+
? record.reasoning_content
|
|
313
|
+
: typeof record.reasoning === "string" && record.reasoning.length > 0
|
|
314
|
+
? record.reasoning
|
|
315
|
+
: undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
319
|
+
return {
|
|
320
|
+
type: "error",
|
|
321
|
+
message: "upstream response contained invalid choices",
|
|
322
|
+
...(usage !== undefined ? { usage } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function invalidToolCallsEvent(
|
|
327
|
+
rawToolCalls: unknown,
|
|
328
|
+
mode: "stream" | "response",
|
|
329
|
+
usage?: OcxUsage,
|
|
330
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
331
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
332
|
+
// The streamed accumulator knows things a rescan cannot: which field on which pending call
|
|
333
|
+
// was actually rejected. Without the override, a stream carrying accepted padding on call 0
|
|
334
|
+
// and a real defect on call 1 blames call 0, because the stateless scan stops at the first
|
|
335
|
+
// structurally odd value it sees.
|
|
336
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
337
|
+
const detail = diagnostic
|
|
338
|
+
? ` (${diagnostic.reason}${diagnostic.callIndex !== undefined ? `; callIndex=${diagnostic.callIndex}` : ""}; valueType=${diagnostic.valueType})`
|
|
339
|
+
: "";
|
|
340
|
+
return {
|
|
341
|
+
type: "error",
|
|
342
|
+
status: 502,
|
|
343
|
+
errorType: "upstream_error",
|
|
344
|
+
message: `upstream response contained invalid tool calls${detail}`,
|
|
345
|
+
...(usage !== undefined ? { usage } : {}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A streamed tool call is only dispatchable once the upstream has named the function.
|
|
351
|
+
*
|
|
352
|
+
* The OpenAI streaming convention puts `function.name` in the first chunk for a tool-call
|
|
353
|
+
* index and leaves later chunks carrying only `arguments` deltas, so a stream that never
|
|
354
|
+
* sends a name is non-conforming for every provider rather than quirky for one. The
|
|
355
|
+
* reference implementations accumulate such a call with an empty name and let the caller
|
|
356
|
+
* fail; we sit at the boundary where it would become a Codex tool-call contract event, so
|
|
357
|
+
* the equivalent is to refuse to emit it.
|
|
358
|
+
*
|
|
359
|
+
* Failing closed rather than dropping is deliberate, and matches #1325: a claimed tool call
|
|
360
|
+
* that silently disappears can leave the matching result orphaned on the next turn. Naming
|
|
361
|
+
* it ourselves is worse still — the id is synthesizable because it is an opaque correlation
|
|
362
|
+
* handle, but a function name is a guess at intent.
|
|
363
|
+
*/
|
|
364
|
+
function unnamedToolCallEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
|
|
365
|
+
return {
|
|
366
|
+
type: "error",
|
|
367
|
+
message: "upstream streamed a tool call without a function name — cannot dispatch",
|
|
368
|
+
...(usage !== undefined ? { usage } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
373
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
type InvalidToolCallReason =
|
|
377
|
+
| "tool_calls_not_array"
|
|
378
|
+
| "tool_call_not_object"
|
|
379
|
+
| "tool_call_id_invalid"
|
|
380
|
+
| "tool_call_function_not_object"
|
|
381
|
+
| "tool_call_function_name_invalid"
|
|
382
|
+
| "tool_call_function_name_blank"
|
|
383
|
+
| "tool_call_function_arguments_invalid";
|
|
384
|
+
|
|
385
|
+
type InvalidToolCallDiagnostic = {
|
|
386
|
+
reason: InvalidToolCallReason;
|
|
387
|
+
callIndex?: number;
|
|
388
|
+
valueType: string;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
type InvalidFieldShape =
|
|
392
|
+
| {
|
|
393
|
+
kind: "object";
|
|
394
|
+
knownKeys: string[];
|
|
395
|
+
knownFieldTypes: Record<string, string>;
|
|
396
|
+
hasUnknownKeys: boolean;
|
|
397
|
+
}
|
|
398
|
+
| {
|
|
399
|
+
kind: "array";
|
|
400
|
+
length: number;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const SAFE_TOOL_CALL_SHAPE_KEYS = [
|
|
404
|
+
"name",
|
|
405
|
+
"type",
|
|
406
|
+
"value",
|
|
407
|
+
"function",
|
|
408
|
+
"arguments",
|
|
409
|
+
"id",
|
|
410
|
+
"index",
|
|
411
|
+
] as const;
|
|
412
|
+
const SAFE_TOOL_CALL_SHAPE_KEY_SET = new Set<string>(SAFE_TOOL_CALL_SHAPE_KEYS);
|
|
413
|
+
|
|
414
|
+
function structuralValueType(value: unknown): string {
|
|
415
|
+
return value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function invalidToolCallField(rawToolCalls: unknown, diagnostic: InvalidToolCallDiagnostic): unknown {
|
|
419
|
+
if (diagnostic.reason === "tool_calls_not_array") return rawToolCalls;
|
|
420
|
+
if (!Array.isArray(rawToolCalls) || diagnostic.callIndex === undefined) return undefined;
|
|
421
|
+
|
|
422
|
+
const rawToolCall = rawToolCalls[diagnostic.callIndex];
|
|
423
|
+
if (diagnostic.reason === "tool_call_not_object") return rawToolCall;
|
|
424
|
+
if (!isRecord(rawToolCall)) return undefined;
|
|
425
|
+
if (diagnostic.reason === "tool_call_function_not_object") return rawToolCall.function;
|
|
426
|
+
|
|
427
|
+
const rawFunction = rawToolCall.function;
|
|
428
|
+
switch (diagnostic.reason) {
|
|
429
|
+
case "tool_call_id_invalid":
|
|
430
|
+
return rawToolCall.id;
|
|
431
|
+
case "tool_call_function_name_invalid":
|
|
432
|
+
return isRecord(rawFunction) ? rawFunction.name : undefined;
|
|
433
|
+
case "tool_call_function_arguments_invalid":
|
|
434
|
+
return isRecord(rawFunction) ? rawFunction.arguments : undefined;
|
|
435
|
+
default:
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function fingerprintInvalidField(value: unknown): InvalidFieldShape | undefined {
|
|
441
|
+
if (Array.isArray(value)) return { kind: "array", length: value.length };
|
|
442
|
+
if (!isRecord(value)) return undefined;
|
|
443
|
+
|
|
444
|
+
const knownKeys: string[] = [];
|
|
445
|
+
const knownFieldTypes: Record<string, string> = {};
|
|
446
|
+
for (const key of SAFE_TOOL_CALL_SHAPE_KEYS) {
|
|
447
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
448
|
+
knownKeys.push(key);
|
|
449
|
+
knownFieldTypes[key] = structuralValueType(value[key]);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let hasUnknownKeys = false;
|
|
453
|
+
for (const key of Object.keys(value)) {
|
|
454
|
+
if (!SAFE_TOOL_CALL_SHAPE_KEY_SET.has(key)) {
|
|
455
|
+
hasUnknownKeys = true;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { kind: "object", knownKeys, knownFieldTypes, hasUnknownKeys };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Streamed string fields are absent when null or undefined (#1731): OpenAI-compatible
|
|
464
|
+
* streamers repeat already-sent `id`/`name`/`arguments` as null on continuation deltas.
|
|
465
|
+
* The accumulator and this diagnostic share this predicate so they cannot disagree about
|
|
466
|
+
* which delta was the invalid one.
|
|
467
|
+
*/
|
|
468
|
+
function isInvalidStreamStringField(value: unknown): boolean {
|
|
469
|
+
return value != null && typeof value !== "string";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Explain only the rejected wire shape, never its values. This diagnostic exists so provider
|
|
474
|
+
* compatibility can be tightened from evidence without retaining tool arguments or credentials.
|
|
475
|
+
*/
|
|
476
|
+
function diagnoseInvalidToolCalls(
|
|
477
|
+
rawToolCalls: unknown,
|
|
478
|
+
mode: "stream" | "response",
|
|
479
|
+
): InvalidToolCallDiagnostic | undefined {
|
|
480
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
481
|
+
return { reason: "tool_calls_not_array", valueType: rawToolCalls === null ? "null" : typeof rawToolCalls };
|
|
482
|
+
}
|
|
483
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
484
|
+
const rawToolCall = rawToolCalls[callIndex];
|
|
485
|
+
if (!isRecord(rawToolCall)) {
|
|
486
|
+
return {
|
|
487
|
+
reason: "tool_call_not_object",
|
|
488
|
+
callIndex,
|
|
489
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (mode === "stream") {
|
|
493
|
+
// The streamed path validates the pieces it is about to store (#1531): a present
|
|
494
|
+
// `function` must be a record, and a present `name`/`arguments`/`id` must be a string.
|
|
495
|
+
// Blank names are caught later at flush, not here, so they are not diagnosed on this
|
|
496
|
+
// branch. Describe exactly that boundary rather than tightening compatibility in a
|
|
497
|
+
// diagnostic change.
|
|
498
|
+
// #1731: "present" means the same thing here as in the accumulator — null and undefined
|
|
499
|
+
// are both absent, because some OpenAI-compatible streamers repeat already-sent fields
|
|
500
|
+
// as null on continuation deltas. A separate predicate here would diagnose accepted
|
|
501
|
+
// padding as the failure and point compatibility work at the wrong delta.
|
|
502
|
+
const streamFunction = (rawToolCall as { function?: unknown }).function;
|
|
503
|
+
if (streamFunction !== undefined && streamFunction !== null) {
|
|
504
|
+
if (!isRecord(streamFunction)) {
|
|
505
|
+
return {
|
|
506
|
+
reason: "tool_call_function_not_object",
|
|
507
|
+
callIndex,
|
|
508
|
+
valueType: Array.isArray(streamFunction) ? "array" : typeof streamFunction,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (isInvalidStreamStringField(streamFunction.name)) {
|
|
512
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof streamFunction.name };
|
|
513
|
+
}
|
|
514
|
+
if (isInvalidStreamStringField(streamFunction.arguments)) {
|
|
515
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof streamFunction.arguments };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (isInvalidStreamStringField(rawToolCall.id)) {
|
|
519
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Precedence must mirror the buffered validator below, or a payload with more than one
|
|
524
|
+
// problem is reported under the wrong reason and sends compatibility work after the wrong
|
|
525
|
+
// shape. That validator checks the `function` container first (`!isRecord(rawToolCall) ||
|
|
526
|
+
// !isRecord(rawToolCall.function)`), then id/name/arguments types together, and only then
|
|
527
|
+
// the blank name.
|
|
528
|
+
if (!isRecord(rawToolCall.function)) {
|
|
529
|
+
return {
|
|
530
|
+
reason: "tool_call_function_not_object",
|
|
531
|
+
callIndex,
|
|
532
|
+
valueType: rawToolCall.function === null ? "null" : Array.isArray(rawToolCall.function) ? "array" : typeof rawToolCall.function,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (typeof rawToolCall.id !== "string") {
|
|
536
|
+
return { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawToolCall.id };
|
|
537
|
+
}
|
|
538
|
+
if (typeof rawToolCall.function.name !== "string") {
|
|
539
|
+
return { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawToolCall.function.name };
|
|
540
|
+
}
|
|
541
|
+
if (typeof rawToolCall.function.arguments !== "string") {
|
|
542
|
+
return { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawToolCall.function.arguments };
|
|
543
|
+
}
|
|
544
|
+
// Last, matching the validator: #1531 also rejects a blank or whitespace-only name here,
|
|
545
|
+
// because such a call cannot select a dispatch target. Reporting it as `name_invalid`
|
|
546
|
+
// would claim a type problem for a correctly-typed value, so it gets its own code.
|
|
547
|
+
if (rawToolCall.function.name.trim().length === 0) {
|
|
548
|
+
return { reason: "tool_call_function_name_blank", callIndex, valueType: "string" };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function logInvalidToolCalls(
|
|
555
|
+
mode: "stream" | "response",
|
|
556
|
+
rawToolCalls: unknown,
|
|
557
|
+
diagnosticOverride?: InvalidToolCallDiagnostic,
|
|
558
|
+
): void {
|
|
559
|
+
if (!isDebugEnabled()) return;
|
|
560
|
+
const diagnostic = diagnosticOverride ?? diagnoseInvalidToolCalls(rawToolCalls, mode);
|
|
561
|
+
if (!diagnostic) return;
|
|
562
|
+
const fieldShape = fingerprintInvalidField(invalidToolCallField(rawToolCalls, diagnostic));
|
|
563
|
+
debugProviderDiagnostic("openai-chat", "invalid-tool-calls", {
|
|
564
|
+
mode,
|
|
565
|
+
...diagnostic,
|
|
566
|
+
...(fieldShape ? { fieldShape } : {}),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
73
570
|
function developerSystemText(message: OcxMessage): string | undefined {
|
|
74
571
|
if (message.role !== "developer") return undefined;
|
|
75
572
|
if (typeof message.content === "string") return message.content;
|
|
@@ -77,20 +574,49 @@ function developerSystemText(message: OcxMessage): string | undefined {
|
|
|
77
574
|
return message.content.map(part => (part as OcxTextContent).text).join("");
|
|
78
575
|
}
|
|
79
576
|
|
|
577
|
+
function isNativeOpenAIChatTarget(provider: OcxProviderConfig): boolean {
|
|
578
|
+
try {
|
|
579
|
+
return new URL(provider.baseUrl).hostname === "api.openai.com";
|
|
580
|
+
} catch {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
587
|
+
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
588
|
+
* being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
|
|
589
|
+
* URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
|
|
590
|
+
*/
|
|
591
|
+
function toolResultTextForWire(content: string | OcxContentPart[]): string {
|
|
592
|
+
if (typeof content === "string") return content;
|
|
593
|
+
const text = content.filter((p) => p.type === "text").map((p) => (p as OcxTextContent).text).join("");
|
|
594
|
+
if (text) {
|
|
595
|
+
const untransportableImages = content.filter((p) => p.type === "image" && !p.imageUrl).length;
|
|
596
|
+
return `${text}${"[image]".repeat(untransportableImages)}`;
|
|
597
|
+
}
|
|
598
|
+
return contentPartsToText(content);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
|
|
602
|
+
if (typeof content === "string") return [];
|
|
603
|
+
const parts: unknown[] = [];
|
|
604
|
+
for (const p of content) {
|
|
605
|
+
if (p.type !== "image" || !p.imageUrl) continue;
|
|
606
|
+
parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
|
|
607
|
+
}
|
|
608
|
+
return parts;
|
|
609
|
+
}
|
|
610
|
+
|
|
80
611
|
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
81
612
|
const out: unknown[] = [];
|
|
82
613
|
const { context, options } = parsed;
|
|
614
|
+
const replayCacheScope = parsed._reasoningReplayScope;
|
|
83
615
|
|
|
84
|
-
// 260718 dangling tool_calls hardening (devlog/_plan/260718_dangling_toolcall_hardening):
|
|
85
|
-
// strict chat providers (Kimi/Moonshot) 400 when an assistant tool_call is not answered
|
|
86
|
-
// immediately by role:"tool" messages. Repair order: (1) reattach a real result to its
|
|
87
|
-
// original call (barrier messages are DEFERRED until the open tool round closes),
|
|
88
|
-
// (2) synthesize an explicit unavailable-result only when no real result exists,
|
|
89
|
-
// (3) manufacture an orphan assistant call only when no call occurrence matches at all.
|
|
90
|
-
// Occurrences are kept as an ordered list (never a Map) so duplicated ids survive.
|
|
91
616
|
interface PendingToolCall { id: string; name: string }
|
|
92
617
|
let pendingToolCalls: PendingToolCall[] = [];
|
|
93
618
|
let deferredBarrierMessages: unknown[] = [];
|
|
619
|
+
let pendingToolResultImageParts: unknown[] = [];
|
|
94
620
|
let mintedIdSeq = 0;
|
|
95
621
|
const seenWireCallIds = new Set<string>();
|
|
96
622
|
|
|
@@ -109,9 +635,18 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
109
635
|
deferredBarrierMessages = [];
|
|
110
636
|
};
|
|
111
637
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
638
|
+
const flushToolResultImages = (): void => {
|
|
639
|
+
if (pendingToolResultImageParts.length === 0) return;
|
|
640
|
+
out.push({
|
|
641
|
+
role: "user",
|
|
642
|
+
content: [
|
|
643
|
+
{ type: "text", text: "[ocx] image output from the preceding tool result(s):" },
|
|
644
|
+
...pendingToolResultImageParts,
|
|
645
|
+
],
|
|
646
|
+
});
|
|
647
|
+
pendingToolResultImageParts = [];
|
|
648
|
+
};
|
|
649
|
+
|
|
115
650
|
const flushPendingToolCalls = (): void => {
|
|
116
651
|
if (pendingToolCalls.length === 0) return;
|
|
117
652
|
for (const call of pendingToolCalls) {
|
|
@@ -122,32 +657,29 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
122
657
|
});
|
|
123
658
|
}
|
|
124
659
|
pendingToolCalls = [];
|
|
660
|
+
flushToolResultImages();
|
|
125
661
|
releaseDeferredBarriers();
|
|
126
662
|
};
|
|
127
663
|
|
|
664
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
128
665
|
const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
|
|
129
666
|
? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
|
|
130
667
|
: undefined;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// vision messages at their original position below.
|
|
137
|
-
const developerSystemParts = context.messages
|
|
138
|
-
.map(developerSystemText)
|
|
139
|
-
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
668
|
+
const developerSystemParts = nativeOpenAI
|
|
669
|
+
? []
|
|
670
|
+
: context.messages
|
|
671
|
+
.map(developerSystemText)
|
|
672
|
+
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
140
673
|
const systemParts = [
|
|
141
674
|
...(context.systemPrompt ?? []),
|
|
142
675
|
...developerSystemParts,
|
|
143
676
|
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
144
677
|
];
|
|
145
678
|
if (systemParts.length > 0) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const sys = neutralizeIdentity(systemParts.join("\n\n"));
|
|
679
|
+
const wireModelId = provider.modelSuffixBracketStrip
|
|
680
|
+
? stripBracketedModelSuffix(parsed.modelId)
|
|
681
|
+
: parsed.modelId;
|
|
682
|
+
const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
|
|
151
683
|
out.push({ role: "system", content: sys });
|
|
152
684
|
}
|
|
153
685
|
|
|
@@ -157,24 +689,23 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
157
689
|
case "developer": {
|
|
158
690
|
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
159
691
|
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
160
|
-
if (msg.role === "developer" && !hasImages) break;
|
|
161
692
|
let chatMsg: Record<string, unknown>;
|
|
162
|
-
if (
|
|
693
|
+
if (msg.role === "developer" && !hasImages) {
|
|
694
|
+
if (!nativeOpenAI) break;
|
|
695
|
+
const text = typeof msg.content === "string"
|
|
696
|
+
? msg.content
|
|
697
|
+
: parts!.map(p => (p as OcxTextContent).text).join("");
|
|
698
|
+
chatMsg = { role: "developer", content: text };
|
|
699
|
+
} else if (typeof msg.content === "string") {
|
|
163
700
|
chatMsg = { role: "user", content: msg.content };
|
|
701
|
+
} else if (!hasImages) {
|
|
702
|
+
chatMsg = { role: "user", content: parts!.map(p => (p as OcxTextContent).text).join("") };
|
|
164
703
|
} else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// and the data URL goes straight into image_url.url (never the token-exploding text path).
|
|
170
|
-
const chatParts = parts!.map(p => p.type === "image"
|
|
171
|
-
? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
|
|
172
|
-
: { type: "text", text: (p as OcxTextContent).text });
|
|
173
|
-
chatMsg = { role: "user", content: chatParts };
|
|
174
|
-
}
|
|
704
|
+
const chatParts = parts!.map(p => p.type === "image"
|
|
705
|
+
? { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } }
|
|
706
|
+
: { type: "text", text: (p as OcxTextContent).text });
|
|
707
|
+
chatMsg = { role: "user", content: chatParts };
|
|
175
708
|
}
|
|
176
|
-
// A barrier must not split an open tool round: defer it until the round closes
|
|
177
|
-
// (real result arrives) or the round is synthesized shut.
|
|
178
709
|
if (pendingToolCalls.length > 0) deferredBarrierMessages.push(chatMsg);
|
|
179
710
|
else out.push(chatMsg);
|
|
180
711
|
break;
|
|
@@ -185,18 +716,38 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
185
716
|
const thinkingParts = aMsg.content.filter(p => p.type === "thinking") as OcxThinkingContent[];
|
|
186
717
|
const toolCalls = aMsg.content.filter(p => p.type === "toolCall") as OcxToolCall[];
|
|
187
718
|
const chatMsg: Record<string, unknown> = { role: "assistant" };
|
|
188
|
-
if (textParts.length > 0)
|
|
189
|
-
|
|
719
|
+
if (textParts.length > 0) chatMsg.content = textParts.map(p => p.text).join("");
|
|
720
|
+
let reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
721
|
+
if (
|
|
722
|
+
reasoningContent.length === 0
|
|
723
|
+
&& toolCalls.length > 0
|
|
724
|
+
&& modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
725
|
+
) {
|
|
726
|
+
const cached = toolCalls
|
|
727
|
+
.map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
|
|
728
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0);
|
|
729
|
+
// Parallel calls share one preceding reasoning block, which is
|
|
730
|
+
// recorded under every call id — join unique texts only.
|
|
731
|
+
if (cached.length > 0) {
|
|
732
|
+
reasoningContent = [...new Set(cached)].join("\n");
|
|
733
|
+
} else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
734
|
+
// Fallback (extends #950, closes #1193): the replay cache is
|
|
735
|
+
// bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
|
|
736
|
+
// long sessions, and some tool rounds carry no recorded reasoning
|
|
737
|
+
// at all. DeepSeek thinking mode rejects ANY tool_call assistant
|
|
738
|
+
// message missing reasoning_content with HTTP 400, so inject a
|
|
739
|
+
// minimal placeholder rather than emit a bare continuation the
|
|
740
|
+
// upstream will reject. Scoped to requiresReasoningPlaceholderModels
|
|
741
|
+
// (defaulting to the preserve list): preserve-listed providers with
|
|
742
|
+
// toggleable thinking (MiniMax low effort) opt out with `[]` so
|
|
743
|
+
// non-thinking histories are never given a fabricated placeholder.
|
|
744
|
+
reasoningContent = " ";
|
|
745
|
+
}
|
|
190
746
|
}
|
|
191
|
-
const reasoningContent = thinkingParts.map(p => p.thinking).join("");
|
|
192
747
|
if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
|
|
193
748
|
chatMsg.reasoning_content = reasoningContent;
|
|
194
749
|
}
|
|
195
|
-
// Skip empty assistant messages: chat APIs like DeepSeek reject an assistant message
|
|
196
|
-
// with neither content, tool calls, nor a provider-supported reasoning_content field.
|
|
197
750
|
if (chatMsg.content === undefined && toolCalls.length === 0 && chatMsg.reasoning_content === undefined) break;
|
|
198
|
-
// A new assistant starts while a previous round is still open: close the previous
|
|
199
|
-
// round synthetically first so its tool_calls are never left dangling.
|
|
200
751
|
flushPendingToolCalls();
|
|
201
752
|
const wireToolCalls = toolCalls.map(tc => {
|
|
202
753
|
let id = tc.id;
|
|
@@ -210,8 +761,6 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
210
761
|
type: "function",
|
|
211
762
|
function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
|
|
212
763
|
}));
|
|
213
|
-
// "" instead of null: strict validators (xAI: "Each message must have at least one
|
|
214
|
-
// content element", langchain#34140) reject content-less assistant history entries.
|
|
215
764
|
if (!chatMsg.content) chatMsg.content = emptyAssistantContent(provider);
|
|
216
765
|
}
|
|
217
766
|
if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
|
|
@@ -225,26 +774,44 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
225
774
|
let toolCallId = msg.toolCallId;
|
|
226
775
|
const matchIdx = toolCallId ? pendingToolCalls.findIndex(c => c.id === toolCallId) : -1;
|
|
227
776
|
if (matchIdx >= 0 && toolCallId) {
|
|
228
|
-
// Real result reattached to its original call. Barriers were deferred, so the
|
|
229
|
-
// tool message lands immediately inside the open round.
|
|
230
777
|
out.push({
|
|
231
778
|
role: "tool",
|
|
232
779
|
tool_call_id: toolCallId,
|
|
233
|
-
content:
|
|
780
|
+
content: toolResultTextForWire(msg.content),
|
|
234
781
|
});
|
|
782
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
235
783
|
pendingToolCalls.splice(matchIdx, 1);
|
|
236
|
-
if (pendingToolCalls.length === 0)
|
|
784
|
+
if (pendingToolCalls.length === 0) {
|
|
785
|
+
flushToolResultImages();
|
|
786
|
+
releaseDeferredBarriers();
|
|
787
|
+
}
|
|
237
788
|
} else {
|
|
238
789
|
if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
|
|
239
|
-
// No matching call in the open round. Close any unresolved round first so the
|
|
240
|
-
// synthesized orphan pair never splits it, then keep the historical repair:
|
|
241
|
-
// WS turns can arrive with only tool outputs; chat-completions providers reject a bare
|
|
242
|
-
// role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
|
|
243
790
|
flushPendingToolCalls();
|
|
244
791
|
const name = safeToolName(msg.toolName);
|
|
792
|
+
const cachedReasoning =
|
|
793
|
+
toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
794
|
+
? peekReasoningForCall(toolCallId, replayCacheScope)
|
|
795
|
+
: undefined;
|
|
796
|
+
// Same fallback as the main-assistant path: never emit a bare orphan
|
|
797
|
+
// tool_call continuation on a thinking-mode provider — inject a
|
|
798
|
+
// placeholder when the replay cache missed (the bounded cache can
|
|
799
|
+
// always miss on long sessions), or DeepSeek thinking mode 400s.
|
|
800
|
+
// Gate on the preserve list too: reasoning_content is only ever
|
|
801
|
+
// serialized for preserve-listed models, so a requires-only custom
|
|
802
|
+
// entry must not fabricate it on this path (P2 on #1205).
|
|
803
|
+
// `||` (not `??`): the cache never stores empty strings, but treat a
|
|
804
|
+
// falsy hit as a miss so the placeholder still fires.
|
|
805
|
+
const orphanReasoning =
|
|
806
|
+
cachedReasoning
|
|
807
|
+
|| (modelInList(provider.preserveReasoningContentModels, parsed.modelId)
|
|
808
|
+
&& modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
|
|
809
|
+
? " "
|
|
810
|
+
: undefined);
|
|
245
811
|
out.push({
|
|
246
812
|
role: "assistant",
|
|
247
813
|
content: emptyAssistantContent(provider),
|
|
814
|
+
...(orphanReasoning ? { reasoning_content: orphanReasoning } : {}),
|
|
248
815
|
tool_calls: [{
|
|
249
816
|
id: toolCallId,
|
|
250
817
|
type: "function",
|
|
@@ -255,16 +822,16 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
|
|
|
255
822
|
out.push({
|
|
256
823
|
role: "tool",
|
|
257
824
|
tool_call_id: toolCallId,
|
|
258
|
-
content:
|
|
825
|
+
content: toolResultTextForWire(msg.content),
|
|
259
826
|
});
|
|
827
|
+
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
828
|
+
flushToolResultImages();
|
|
260
829
|
}
|
|
261
830
|
break;
|
|
262
831
|
}
|
|
263
832
|
}
|
|
264
833
|
}
|
|
265
834
|
|
|
266
|
-
// Trailing dangle: a turn interrupted after the assistant requested tools leaves the
|
|
267
|
-
// round open; close it synthetically (then release any deferred barriers in order).
|
|
268
835
|
flushPendingToolCalls();
|
|
269
836
|
releaseDeferredBarriers();
|
|
270
837
|
return out;
|
|
@@ -360,133 +927,354 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
|
360
927
|
|| baseUrl === "https://opencode.ai/zen/go/v1";
|
|
361
928
|
}
|
|
362
929
|
|
|
363
|
-
|
|
930
|
+
/** Azure Model Router (and Gemini-in-the-pool) 400s Codex MCP schemas whose root is a union. */
|
|
931
|
+
const AZURE_CHAT_FORBIDDEN_ROOT_KEYS = ["oneOf", "anyOf", "allOf", "enum", "const", "not"] as const;
|
|
364
932
|
|
|
365
|
-
function
|
|
933
|
+
function isAzureOpenAiChatTarget(provider: OcxProviderConfig): boolean {
|
|
366
934
|
try {
|
|
367
|
-
|
|
935
|
+
const host = new URL(provider.baseUrl).hostname.toLowerCase();
|
|
936
|
+
return host.endsWith(".openai.azure.com")
|
|
937
|
+
|| host.endsWith(".cognitiveservices.azure.com")
|
|
938
|
+
|| host.endsWith(".services.ai.azure.com")
|
|
939
|
+
|| host.endsWith(".ai.azure.com");
|
|
368
940
|
} catch {
|
|
369
941
|
return false;
|
|
370
942
|
}
|
|
371
943
|
}
|
|
372
944
|
|
|
373
|
-
|
|
945
|
+
/**
|
|
946
|
+
* Azure Foundry Model Router validates every function schema against the strictest model in
|
|
947
|
+
* the pool (Gemini-shaped): root must be {type:"object"} with no oneOf/anyOf/allOf/enum/
|
|
948
|
+
* const/not. Codex App MCP tools such as mcp__codex_app__automation_update ship a root
|
|
949
|
+
* union, which 400s the whole turn. Flatten like Zen, then strip leftover forbidden keys.
|
|
950
|
+
*/
|
|
951
|
+
function sanitizeAzureChatToolParameters(parameters: unknown): Record<string, unknown> {
|
|
952
|
+
const root = ensureZenRootObjectSchema(parameters);
|
|
953
|
+
for (const key of AZURE_CHAT_FORBIDDEN_ROOT_KEYS) delete root[key];
|
|
954
|
+
root.type = "object";
|
|
955
|
+
if (!root.properties || typeof root.properties !== "object" || Array.isArray(root.properties)) {
|
|
956
|
+
root.properties = {};
|
|
957
|
+
}
|
|
958
|
+
return root;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Moonshot validates function schemas against a draft-07 reading of `$ref`, where the
|
|
962
|
+
// keyword stands alone and siblings are ignored. It rejects the whole request rather
|
|
963
|
+
// than ignoring them: "not a valid moonshot flavored json schema ... when using $ref,
|
|
964
|
+
// type should be defined in the referenced schema instead of the parent schema".
|
|
965
|
+
const MOONSHOT_SCHEMA_HOSTNAMES = new Set([
|
|
966
|
+
"api.kimi.com",
|
|
967
|
+
"api.moonshot.ai",
|
|
968
|
+
"api.moonshot.cn",
|
|
969
|
+
]);
|
|
970
|
+
|
|
971
|
+
function isMoonshotSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
374
972
|
try {
|
|
375
|
-
return new URL(provider.baseUrl).hostname
|
|
973
|
+
return MOONSHOT_SCHEMA_HOSTNAMES.has(new URL(provider.baseUrl).hostname);
|
|
376
974
|
} catch {
|
|
377
975
|
return false;
|
|
378
976
|
}
|
|
379
977
|
}
|
|
380
978
|
|
|
381
|
-
// Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
|
|
382
|
-
// REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
|
|
383
|
-
// `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
|
|
384
|
-
// "", and xAI actively requires it ("Each message must have at least one content element"), so
|
|
385
|
-
// the two contracts are in direct conflict and this cannot be a global change.
|
|
386
979
|
const VOLCENGINE_ARK_HOSTNAMES = new Set([
|
|
387
980
|
"ark.cn-beijing.volces.com",
|
|
388
981
|
"ark.ap-southeast.volces.com",
|
|
389
982
|
]);
|
|
390
983
|
|
|
391
|
-
function
|
|
984
|
+
function isVolcengineArkPaygChatTarget(provider: OcxProviderConfig): boolean {
|
|
392
985
|
try {
|
|
393
|
-
|
|
986
|
+
const url = new URL(provider.baseUrl);
|
|
987
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
988
|
+
return VOLCENGINE_ARK_HOSTNAMES.has(url.hostname) && pathname === "/api/v3";
|
|
394
989
|
} catch {
|
|
395
990
|
return false;
|
|
396
991
|
}
|
|
397
992
|
}
|
|
398
993
|
|
|
994
|
+
function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "text"; text: string }[] {
|
|
995
|
+
return isVolcengineArkPaygChatTarget(provider) ? [{ type: "text", text: "" }] : "";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
999
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
1000
|
+
return { type: "object", properties: {} };
|
|
1001
|
+
}
|
|
1002
|
+
const obj = parameters as Record<string, unknown>;
|
|
1003
|
+
if (obj.type === "object") return obj;
|
|
1004
|
+
return { ...obj, type: "object" };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
|
|
1008
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* JSON Schema 2020-12 makes `$ref` an in-place applicator: siblings stay in force and are
|
|
1013
|
+
* combined with the referenced schema. Moonshot enforces the older draft-07 reading where
|
|
1014
|
+
* `$ref` must stand alone, and 400s the entire request when a node carries both. Codex's own
|
|
1015
|
+
* deferred tool catalog emits exactly that shape (zod-to-json-schema deduplicates into
|
|
1016
|
+
* `$defs.__schema*` nodes that keep `type`/`minLength`/`format` beside the `$ref`), so the
|
|
1017
|
+
* schema is not something a user can fix from configuration — see issue #2673.
|
|
1018
|
+
*
|
|
1019
|
+
* Inline the referenced schema underneath the node's own keywords, which is what 2020-12 says
|
|
1020
|
+
* the node means, then drop `$ref`. Constraints reach the model instead of being stripped.
|
|
1021
|
+
* The `$defs` bag is preserved: a bare `$ref` (no siblings) is already legal for Moonshot and
|
|
1022
|
+
* is left pointing at its definition rather than expanded, which keeps recursive schemas finite.
|
|
1023
|
+
*/
|
|
1024
|
+
function moonshotRefTargetKeys(node: Record<string, unknown>): string[] {
|
|
1025
|
+
return Object.keys(node).filter(key => key !== "$ref");
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Inlining duplicates the target, so a schema referencing one large definition from many
|
|
1030
|
+
* sibling-carrying nodes can multiply. Bound the total expansions and fall back to a bare
|
|
1031
|
+
* `$ref` once the budget is spent: still valid for Moonshot, just without the node's own
|
|
1032
|
+
* narrowing keywords. Mirrors the node budget in google-tool-schema.ts.
|
|
1033
|
+
*/
|
|
1034
|
+
const MOONSHOT_MAX_REF_EXPANSIONS = 512;
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Expansion count alone does not bound the walk: a deeply nested ref-free schema, or one
|
|
1038
|
+
* large definition repeated across many nodes, still recurses to exhaustion or amplifies the
|
|
1039
|
+
* emitted output. Depth and node budgets close both, and mirror google-tool-schema.ts.
|
|
1040
|
+
*/
|
|
1041
|
+
const MOONSHOT_MAX_SCHEMA_DEPTH = 64;
|
|
1042
|
+
const MOONSHOT_MAX_SCHEMA_NODES = 4_096;
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Assertion keywords whose meaning under a `$ref` is CONJUNCTION, not replacement. A node
|
|
1046
|
+
* carrying `required: ["b"]` beside a target requiring `["a"]` means both are required;
|
|
1047
|
+
* letting the sibling win emitted a schema that no longer described the tool.
|
|
1048
|
+
*/
|
|
1049
|
+
function unionRequired(target: unknown, sibling: unknown): unknown {
|
|
1050
|
+
if (!Array.isArray(target) || !Array.isArray(sibling)) return sibling;
|
|
1051
|
+
const seen = new Set<unknown>();
|
|
1052
|
+
const out: unknown[] = [];
|
|
1053
|
+
for (const name of [...target, ...sibling]) {
|
|
1054
|
+
if (seen.has(name)) continue;
|
|
1055
|
+
seen.add(name);
|
|
1056
|
+
out.push(name);
|
|
1057
|
+
}
|
|
1058
|
+
return out;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
399
1061
|
/**
|
|
400
|
-
*
|
|
1062
|
+
* Keywords whose values are DATA, not schemas.
|
|
401
1063
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
1064
|
+
* Recursing into them rewrote user data: an `enum` listing a literal object that happens
|
|
1065
|
+
* to carry a `"$ref"` string had that key stripped as if it were a schema reference, so a
|
|
1066
|
+
* value the tool declared as legal silently changed shape. These are copied through.
|
|
1067
|
+
*/
|
|
1068
|
+
const MOONSHOT_DATA_VALUED_KEYWORDS = new Set(["enum", "const", "default", "examples"]);
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Numeric assertions whose intersection is a bound, and which direction tightens.
|
|
408
1072
|
*
|
|
409
|
-
*
|
|
1073
|
+
* `$ref` under 2020-12 is an in-place applicator: the node and its target BOTH apply, so
|
|
1074
|
+
* the emitted schema must be their INTERSECTION. The previous code overwrote the target
|
|
1075
|
+
* with the node and called that "the narrower reading", which holds only when the node
|
|
1076
|
+
* happens to be narrower. A node declaring `minLength: 1` beside a target declaring
|
|
1077
|
+
* `minLength: 5` shipped `minLength: 1` - a contract weaker than either side asked for,
|
|
1078
|
+
* emitted silently, which is the same failure mode the `required` composition fixed for
|
|
1079
|
+
* set-valued keywords.
|
|
410
1080
|
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
1081
|
+
* "max" means the surviving value is the larger of the two (lower bounds), "min" the
|
|
1082
|
+
* smaller (upper bounds). A keyword absent from this table keeps the overwrite: for
|
|
1083
|
+
* `type`, `format`, `description` and friends there is no ordering to intersect along,
|
|
1084
|
+
* and the node is the more specific statement.
|
|
413
1085
|
*/
|
|
414
|
-
|
|
415
|
-
|
|
1086
|
+
const MOONSHOT_BOUND_KEYWORDS: Record<string, "max" | "min"> = {
|
|
1087
|
+
minLength: "max",
|
|
1088
|
+
minItems: "max",
|
|
1089
|
+
minProperties: "max",
|
|
1090
|
+
minimum: "max",
|
|
1091
|
+
exclusiveMinimum: "max",
|
|
1092
|
+
maxLength: "min",
|
|
1093
|
+
maxItems: "min",
|
|
1094
|
+
maxProperties: "min",
|
|
1095
|
+
maximum: "min",
|
|
1096
|
+
exclusiveMaximum: "min",
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Intersect one numeric bound. Either side being absent or non-finite yields the other,
|
|
1101
|
+
* because an unstated bound constrains nothing - returning `undefined` there would drop
|
|
1102
|
+
* a constraint the remaining side genuinely made.
|
|
1103
|
+
*/
|
|
1104
|
+
function intersectBound(target: unknown, sibling: unknown, direction: "max" | "min"): unknown {
|
|
1105
|
+
const a = typeof target === "number" && Number.isFinite(target) ? target : null;
|
|
1106
|
+
const b = typeof sibling === "number" && Number.isFinite(sibling) ? sibling : null;
|
|
1107
|
+
if (a === null) return b === null ? sibling : sibling;
|
|
1108
|
+
if (b === null) return target;
|
|
1109
|
+
return direction === "max" ? Math.max(a, b) : Math.min(a, b);
|
|
416
1110
|
}
|
|
417
1111
|
|
|
418
1112
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
1113
|
+
* Compose two `properties` maps. A property named in BOTH the referenced target and the
|
|
1114
|
+
* node is the same conjunction problem `required` had: letting the sibling win discards
|
|
1115
|
+
* the target's constraints for that member. Merge the two member schemas so neither side
|
|
1116
|
+
* loses its keywords. Shared member bounds are the same conjunction one level down,
|
|
1117
|
+
* and nested object members recurse through this helper instead of replacing the target.
|
|
422
1118
|
*/
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
1119
|
+
function composeProperties(
|
|
1120
|
+
target: Record<string, unknown>,
|
|
1121
|
+
sibling: Record<string, unknown>,
|
|
1122
|
+
): Record<string, unknown> {
|
|
1123
|
+
const combined: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
1124
|
+
for (const [name, sub] of Object.entries(target)) combined[name] = sub;
|
|
1125
|
+
for (const [name, sub] of Object.entries(sibling)) {
|
|
1126
|
+
const existing = combined[name];
|
|
1127
|
+
if (isXaiObjectSchema(existing) && isXaiObjectSchema(sub)) {
|
|
1128
|
+
const member: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
1129
|
+
for (const [k, v] of Object.entries(existing)) member[k] = v;
|
|
1130
|
+
for (const [k, v] of Object.entries(sub)) {
|
|
1131
|
+
if (k === "required") {
|
|
1132
|
+
member[k] = unionRequired(member[k], v);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (k === "properties" && isXaiObjectSchema(member[k]) && isXaiObjectSchema(v)) {
|
|
1136
|
+
member[k] = composeProperties(member[k] as Record<string, unknown>, v);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const boundDirection = MOONSHOT_BOUND_KEYWORDS[k];
|
|
1140
|
+
if (boundDirection && k in member) {
|
|
1141
|
+
member[k] = intersectBound(member[k], v, boundDirection);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
member[k] = v;
|
|
1145
|
+
}
|
|
1146
|
+
combined[name] = member;
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
combined[name] = sub;
|
|
426
1150
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1151
|
+
return combined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
interface MoonshotNormalizeState {
|
|
1155
|
+
activeRefs: Set<string>;
|
|
1156
|
+
remainingExpansions: number;
|
|
1157
|
+
remainingNodes: number;
|
|
430
1158
|
}
|
|
431
1159
|
|
|
432
|
-
function
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1160
|
+
function normalizeMoonshotSchemaNode(
|
|
1161
|
+
node: unknown,
|
|
1162
|
+
root: Record<string, unknown>,
|
|
1163
|
+
state: MoonshotNormalizeState,
|
|
1164
|
+
depth = 0,
|
|
1165
|
+
): unknown {
|
|
1166
|
+
if (Array.isArray(node)) {
|
|
1167
|
+
if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH) return [];
|
|
1168
|
+
return node.map(item => normalizeMoonshotSchemaNode(item, root, state, depth + 1));
|
|
439
1169
|
}
|
|
1170
|
+
if (!isXaiObjectSchema(node)) return node;
|
|
440
1171
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1172
|
+
// Fail closed for this node rather than emitting a partially weakened schema: an empty
|
|
1173
|
+
// object is the one shape that asserts nothing it cannot back up.
|
|
1174
|
+
if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH || state.remainingNodes <= 0) return {};
|
|
1175
|
+
state.remainingNodes -= 1;
|
|
1176
|
+
|
|
1177
|
+
const ref = node.$ref;
|
|
1178
|
+
const hasSiblings = moonshotRefTargetKeys(node).length > 0;
|
|
1179
|
+
|
|
1180
|
+
if (typeof ref === "string" && hasSiblings) {
|
|
1181
|
+
// A cycle cannot be inlined. Keeping the bare `$ref` is the lossy-but-valid fallback:
|
|
1182
|
+
// Moonshot accepts it, and the alternative (dropping the ref) would erase the recursion.
|
|
1183
|
+
if (state.activeRefs.has(ref) || state.remainingExpansions <= 0) return { $ref: ref };
|
|
1184
|
+
|
|
1185
|
+
const target = lookupLocalJsonPointer(root, ref);
|
|
1186
|
+
if (isXaiObjectSchema(target)) {
|
|
1187
|
+
state.remainingExpansions -= 1;
|
|
1188
|
+
state.activeRefs.add(ref);
|
|
1189
|
+
const resolvedTarget = normalizeMoonshotSchemaNode(target, root, state, depth + 1);
|
|
1190
|
+
state.activeRefs.delete(ref);
|
|
1191
|
+
const merged: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
1192
|
+
if (isXaiObjectSchema(resolvedTarget)) {
|
|
1193
|
+
for (const [key, value] of Object.entries(resolvedTarget)) merged[key] = value;
|
|
1194
|
+
}
|
|
1195
|
+
// "Alongside the target" is conjunction, not replacement. For most keywords the node
|
|
1196
|
+
// narrows the target and overwriting is the narrower reading, but `required` and
|
|
1197
|
+
// `properties` are set-valued: letting the sibling win DROPPED the target's own
|
|
1198
|
+
// members, so a tool requiring `a` beside a node requiring `b` shipped requiring only
|
|
1199
|
+
// `b`. Those two compose; everything else keeps the narrowing overwrite.
|
|
1200
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1201
|
+
if (key === "$ref") continue;
|
|
1202
|
+
if (MOONSHOT_DATA_VALUED_KEYWORDS.has(key)) {
|
|
1203
|
+
merged[key] = value;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const normalized = normalizeMoonshotSchemaNode(value, root, state, depth + 1);
|
|
1207
|
+
if (key === "required") {
|
|
1208
|
+
merged[key] = unionRequired(merged[key], normalized);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (key === "properties" && isXaiObjectSchema(merged[key]) && isXaiObjectSchema(normalized)) {
|
|
1212
|
+
merged[key] = composeProperties(merged[key] as Record<string, unknown>, normalized);
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
// Numeric bounds intersect rather than overwrite: both the node and its target
|
|
1216
|
+
// apply, so the surviving bound is the stricter of the two in whichever direction
|
|
1217
|
+
// that keyword tightens.
|
|
1218
|
+
const boundDirection = MOONSHOT_BOUND_KEYWORDS[key];
|
|
1219
|
+
if (boundDirection && key in merged) {
|
|
1220
|
+
merged[key] = intersectBound(merged[key], normalized, boundDirection);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
merged[key] = normalized;
|
|
1224
|
+
}
|
|
1225
|
+
return merged;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Unresolvable pointer: a remote ref, a malformed path, or a non-object target. Dropping
|
|
1229
|
+
// the ref and keeping the siblings silently discards whatever the reference constrained,
|
|
1230
|
+
// which is the one outcome we cannot detect downstream. A bare `$ref` is lossy in the
|
|
1231
|
+
// other direction - it loses the node's own keywords - but it preserves the identity of
|
|
1232
|
+
// what was asked for, and Moonshot accepts it.
|
|
1233
|
+
return { $ref: ref };
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
1237
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1238
|
+
out[key] = key === "$ref" || MOONSHOT_DATA_VALUED_KEYWORDS.has(key)
|
|
1239
|
+
? value
|
|
1240
|
+
: normalizeMoonshotSchemaNode(value, root, state, depth + 1);
|
|
449
1241
|
}
|
|
450
|
-
return
|
|
1242
|
+
return out;
|
|
451
1243
|
}
|
|
452
1244
|
|
|
453
|
-
function
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return { ...metadata, oneOf: variants };
|
|
1245
|
+
function normalizeMoonshotToolParameters(parameters: unknown): Record<string, unknown> {
|
|
1246
|
+
const rooted = ensureRootObjectType(parameters);
|
|
1247
|
+
const normalized = normalizeMoonshotSchemaNode(rooted, rooted, {
|
|
1248
|
+
activeRefs: new Set<string>(),
|
|
1249
|
+
remainingExpansions: MOONSHOT_MAX_REF_EXPANSIONS,
|
|
1250
|
+
remainingNodes: MOONSHOT_MAX_SCHEMA_NODES,
|
|
1251
|
+
});
|
|
1252
|
+
return isXaiObjectSchema(normalized) ? normalized : rooted;
|
|
462
1253
|
}
|
|
463
1254
|
|
|
464
1255
|
function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
465
1256
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
466
|
-
const
|
|
467
|
-
? new Set(parsed.options.toolChoice.allowedTools)
|
|
468
|
-
: undefined;
|
|
469
|
-
const tools = allowed
|
|
470
|
-
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
|
|
471
|
-
: parsed.context.tools;
|
|
1257
|
+
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
472
1258
|
if (tools.length === 0) return undefined;
|
|
473
1259
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
474
|
-
const
|
|
1260
|
+
const moonshotTarget = !xaiTarget && isMoonshotSchemaTarget(provider);
|
|
475
1261
|
const formatted = tools.flatMap(t => {
|
|
476
|
-
const
|
|
1262
|
+
const normalized = xaiTarget
|
|
477
1263
|
? normalizeXaiToolParameters(t.parameters)
|
|
478
|
-
:
|
|
479
|
-
?
|
|
480
|
-
: t.parameters;
|
|
1264
|
+
: moonshotTarget
|
|
1265
|
+
? normalizeMoonshotToolParameters(t.parameters)
|
|
1266
|
+
: ensureRootObjectType(t.parameters);
|
|
1267
|
+
const parameters = stripResponsesOnlyEncryptedMarker(normalized);
|
|
1268
|
+
|
|
481
1269
|
if (parameters === undefined) return [];
|
|
482
1270
|
return [{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1271
|
+
type: "function",
|
|
1272
|
+
function: {
|
|
1273
|
+
name: namespacedToolName(t.namespace, t.name),
|
|
1274
|
+
...(t.description ? { description: t.description } : {}),
|
|
1275
|
+
parameters,
|
|
1276
|
+
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
1277
|
+
},
|
|
490
1278
|
}];
|
|
491
1279
|
});
|
|
492
1280
|
return formatted.length > 0 ? formatted : undefined;
|
|
@@ -494,24 +1282,38 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
|
|
|
494
1282
|
|
|
495
1283
|
function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
496
1284
|
const base = toolsToChatFormat(parsed, provider);
|
|
497
|
-
|
|
1285
|
+
const azureChat = isAzureOpenAiChatTarget(provider);
|
|
1286
|
+
const zenChat = shouldSanitizeZenToolParameters(provider);
|
|
1287
|
+
if (!base || (!zenChat && !azureChat)) return base;
|
|
498
1288
|
return base.map(tool => {
|
|
499
1289
|
if (!tool || typeof tool !== "object") return tool;
|
|
500
1290
|
const functionDef = (tool as { function?: Record<string, unknown> }).function;
|
|
501
1291
|
if (!functionDef || typeof functionDef !== "object") return tool;
|
|
1292
|
+
const parameters = azureChat
|
|
1293
|
+
? sanitizeAzureChatToolParameters(functionDef.parameters ?? {})
|
|
1294
|
+
: ensureZenRootObjectSchema(functionDef.parameters ?? {});
|
|
1295
|
+
const nextFunction: Record<string, unknown> = { ...functionDef, parameters };
|
|
1296
|
+
// strict: true plus a flattened schema is rejected by Gemini-in-the-pool routers.
|
|
1297
|
+
if (azureChat) delete nextFunction.strict;
|
|
502
1298
|
return {
|
|
503
1299
|
...tool,
|
|
504
|
-
function:
|
|
505
|
-
...functionDef,
|
|
506
|
-
parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}),
|
|
507
|
-
},
|
|
1300
|
+
function: nextFunction,
|
|
508
1301
|
};
|
|
509
1302
|
});
|
|
510
1303
|
}
|
|
511
1304
|
|
|
512
|
-
function toolChoiceToChatFormat(
|
|
1305
|
+
function toolChoiceToChatFormat(
|
|
1306
|
+
tc: OcxParsedRequest["options"]["toolChoice"],
|
|
1307
|
+
tools: OcxParsedRequest["context"]["tools"],
|
|
1308
|
+
provider: OcxProviderConfig,
|
|
1309
|
+
): unknown {
|
|
513
1310
|
if (!tc) return undefined;
|
|
514
|
-
if (isAllowedToolChoice(tc))
|
|
1311
|
+
if (isAllowedToolChoice(tc)) {
|
|
1312
|
+
if (tc.mode === "required" && tc.allowedTools.length === 1 && isNativeOpenAIChatTarget(provider)) {
|
|
1313
|
+
return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.allowedTools[0]) } };
|
|
1314
|
+
}
|
|
1315
|
+
return tc.mode === "required" ? "required" : "auto";
|
|
1316
|
+
}
|
|
515
1317
|
if (tc === "auto" || tc === "none" || tc === "required") return tc;
|
|
516
1318
|
if ("name" in tc) return { type: "function", function: { name: resolveToolChoiceWireName(tools, tc.name) } };
|
|
517
1319
|
return undefined;
|
|
@@ -549,6 +1351,26 @@ function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: stri
|
|
|
549
1351
|
return fraction === undefined ? undefined : Math.max(1, Math.floor(maxBudget * fraction));
|
|
550
1352
|
}
|
|
551
1353
|
|
|
1354
|
+
function canSerializeOpenAIChatServiceTier(
|
|
1355
|
+
provider: OcxProviderConfig,
|
|
1356
|
+
modelId: string,
|
|
1357
|
+
serviceTier: unknown,
|
|
1358
|
+
tierDecision?: OcxParsedRequest["options"]["tierDecision"],
|
|
1359
|
+
): boolean {
|
|
1360
|
+
if (serviceTier === undefined) return false;
|
|
1361
|
+
if (tierDecision !== undefined) {
|
|
1362
|
+
return tierDecision.kind === "set" || tierDecision.kind === "forward-caller";
|
|
1363
|
+
}
|
|
1364
|
+
// No decision from the router means this call did not go through the tier state machine, so
|
|
1365
|
+
// ask that machine rather than re-deriving a looser answer beside it. The previous fallback
|
|
1366
|
+
// returned true whenever foreign forwarding was allowed at all, which let a caller tier
|
|
1367
|
+
// reach the wire in cases `decideTier` would have dropped — the two paths disagreeing is
|
|
1368
|
+
// precisely the bug, so there is now only one authority.
|
|
1369
|
+
const callerTier = typeof serviceTier === "string" ? serviceTier : undefined;
|
|
1370
|
+
const decision = decideTier(fastPolicyForModel(provider, modelId, undefined, "chat"), undefined, callerTier);
|
|
1371
|
+
return decision.kind === "set" || decision.kind === "forward-caller";
|
|
1372
|
+
}
|
|
1373
|
+
|
|
552
1374
|
export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
553
1375
|
return {
|
|
554
1376
|
name: "openai-chat",
|
|
@@ -556,20 +1378,30 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
556
1378
|
formatErrorBody: formatOpenAIChatErrorBody,
|
|
557
1379
|
|
|
558
1380
|
buildRequest(parsed: OcxParsedRequest) {
|
|
559
|
-
const
|
|
560
|
-
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
561
|
-
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
562
|
-
}
|
|
563
|
-
|
|
1381
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
564
1382
|
const messages = messagesToChatFormat(parsed, provider);
|
|
565
1383
|
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
566
|
-
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
|
|
1384
|
+
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
|
|
567
1385
|
|
|
568
1386
|
const body: Record<string, unknown> = {
|
|
569
1387
|
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
|
|
570
1388
|
messages,
|
|
571
1389
|
stream: parsed.stream,
|
|
572
1390
|
};
|
|
1391
|
+
// A policy-produced canonical decision has already passed capability validation. Without
|
|
1392
|
+
// that decision, a canonical caller value still requires an explicit true capability;
|
|
1393
|
+
// unclassified Chat routes remain behind the caller-forwarding opt-in.
|
|
1394
|
+
const serviceTier = parsed.options.serviceTier;
|
|
1395
|
+
const tierDecision = parsed.options.tierDecision;
|
|
1396
|
+
const canSerializeServiceTier = canSerializeOpenAIChatServiceTier(
|
|
1397
|
+
provider,
|
|
1398
|
+
parsed.modelId,
|
|
1399
|
+
serviceTier,
|
|
1400
|
+
tierDecision,
|
|
1401
|
+
);
|
|
1402
|
+
if (canSerializeServiceTier && serviceTier !== undefined) {
|
|
1403
|
+
body.service_tier = serviceTier;
|
|
1404
|
+
}
|
|
573
1405
|
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
574
1406
|
const maxTokens = resolveMaxTokens(provider, parsed);
|
|
575
1407
|
const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
|
|
@@ -588,10 +1420,44 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
588
1420
|
body.top_p = parsed.options.topP;
|
|
589
1421
|
}
|
|
590
1422
|
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
1423
|
+
const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
|
|
591
1424
|
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
1425
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
592
1426
|
let reasoningLog: AdapterRequest["reasoningLog"];
|
|
593
|
-
if (
|
|
594
|
-
if (
|
|
1427
|
+
if (!reasoningDisabled && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
|
|
1428
|
+
if (nativeOpenAI) {
|
|
1429
|
+
body.reasoning_effort = "none";
|
|
1430
|
+
reasoningLog = {
|
|
1431
|
+
effectiveEffort: "none",
|
|
1432
|
+
wireField: "reasoning_effort",
|
|
1433
|
+
wireValue: "none",
|
|
1434
|
+
};
|
|
1435
|
+
} else {
|
|
1436
|
+
body.reasoning = { enabled: false };
|
|
1437
|
+
reasoningLog = {
|
|
1438
|
+
effectiveEffort: "none",
|
|
1439
|
+
wireField: "reasoning.enabled",
|
|
1440
|
+
wireValue: false,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
} else if (reasoningEffort !== undefined) {
|
|
1444
|
+
if (provider.reasoningWireFormat === "gateway-object") {
|
|
1445
|
+
if (nativeOpenAI) {
|
|
1446
|
+
body.reasoning_effort = reasoningEffort;
|
|
1447
|
+
reasoningLog = {
|
|
1448
|
+
effectiveEffort: reasoningEffort,
|
|
1449
|
+
wireField: "reasoning_effort",
|
|
1450
|
+
wireValue: reasoningEffort,
|
|
1451
|
+
};
|
|
1452
|
+
} else {
|
|
1453
|
+
body.reasoning = { enabled: true, effort: reasoningEffort };
|
|
1454
|
+
reasoningLog = {
|
|
1455
|
+
effectiveEffort: reasoningEffort,
|
|
1456
|
+
wireField: "reasoning.effort",
|
|
1457
|
+
wireValue: reasoningEffort,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
} else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
595
1461
|
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
596
1462
|
if (budget !== undefined) {
|
|
597
1463
|
body.thinking_budget = budget;
|
|
@@ -602,9 +1468,6 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
602
1468
|
};
|
|
603
1469
|
}
|
|
604
1470
|
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
|
|
605
|
-
// Vendor thinking-toggle wire: the mapped value is sent as `thinking: {type}` because
|
|
606
|
-
// these models ignore/reject reasoning_effort. Most use enabled/disabled; MiniMax-M3
|
|
607
|
-
// uses adaptive/disabled.
|
|
608
1471
|
if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
|
|
609
1472
|
body.thinking = { type: reasoningEffort };
|
|
610
1473
|
reasoningLog = {
|
|
@@ -628,37 +1491,55 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
628
1491
|
if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
|
|
629
1492
|
body.frequency_penalty = parsed.options.frequencyPenalty;
|
|
630
1493
|
}
|
|
631
|
-
// prompt_cache_key is an OpenAI-specific chat extension; strict backends (Groq,
|
|
632
|
-
// Cerebras, etc.) reject unknown fields. Only forward when the provider opts in.
|
|
633
1494
|
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
|
|
634
1495
|
body.prompt_cache_key = parsed.options.promptCacheKey;
|
|
635
1496
|
}
|
|
1497
|
+
// Structured-output support varies by the physical upstream model even when one
|
|
1498
|
+
// gateway exposes a uniform OpenAI-compatible endpoint. Keep the #1137 translation
|
|
1499
|
+
// as the default, but let an exact model opt out instead of forcing a provider-wide
|
|
1500
|
+
// rollback that would silently return prose for siblings that support JSON Schema.
|
|
1501
|
+
if (!provider.noStructuredOutputModels?.includes(parsed.modelId)) {
|
|
1502
|
+
const textFormat = parsed.options.textFormat;
|
|
1503
|
+
if (textFormat?.type === "json_object") {
|
|
1504
|
+
body.response_format = { type: "json_object" };
|
|
1505
|
+
} else if (textFormat?.type === "json_schema") {
|
|
1506
|
+
body.response_format = {
|
|
1507
|
+
type: "json_schema",
|
|
1508
|
+
json_schema: {
|
|
1509
|
+
name: textFormat.name ?? "response",
|
|
1510
|
+
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
|
|
1511
|
+
...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
|
|
1512
|
+
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
|
|
1513
|
+
},
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
636
1517
|
|
|
637
1518
|
if (tools) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1519
|
+
if (provider.parallelToolCalls === false) {
|
|
1520
|
+
// NIM documents the Boolean defaulting to false and kimi rejects true; pin the
|
|
1521
|
+
// wire bit so Codex cannot opt in via request.options. Other opted-out providers
|
|
1522
|
+
// omit the field by default so strict OpenAI-compatible hosts never see an
|
|
1523
|
+
// unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
|
|
1524
|
+
// emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
|
|
1525
|
+
if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
|
|
1526
|
+
|| provider.pinParallelToolCallsFalse === true) {
|
|
1527
|
+
body.parallel_tool_calls = false;
|
|
1528
|
+
}
|
|
1529
|
+
} else if (provider.parallelToolCalls === true) {
|
|
1530
|
+
body.parallel_tool_calls = parsed.options.parallelToolCalls !== false;
|
|
1531
|
+
}
|
|
648
1532
|
}
|
|
649
|
-
|
|
650
|
-
const url = `${provider.baseUrl}/chat/completions`;
|
|
651
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
652
|
-
// Precedence preserved from pre-#128 behavior: apiKey Authorization first, then
|
|
653
|
-
// provider.headers may override (user/registry-configured headers win). Registry
|
|
654
|
-
// staticHeaders (e.g. opencode-free x-opencode-client) flow in via derive.ts and
|
|
655
|
-
// never carry Authorization, so keyless providers are unaffected.
|
|
656
|
-
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
657
|
-
if (provider.headers) Object.assign(headers, provider.headers);
|
|
1533
|
+
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
658
1534
|
|
|
659
1535
|
const bodyJson = JSON.stringify(body);
|
|
660
|
-
|
|
661
|
-
|
|
1536
|
+
const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
|
|
1537
|
+
const tierLog = createAdapterTierMetadata(
|
|
1538
|
+
parsed.options.tierObservation,
|
|
1539
|
+
parsed.options.tierDecision,
|
|
1540
|
+
actualServiceTier === null ? null : "service-tier",
|
|
1541
|
+
actualServiceTier,
|
|
1542
|
+
);
|
|
662
1543
|
if (isDebugEnabled()) {
|
|
663
1544
|
let host = "upstream";
|
|
664
1545
|
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
@@ -679,10 +1560,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
679
1560
|
headers,
|
|
680
1561
|
body: bodyJson,
|
|
681
1562
|
...(reasoningLog ? { reasoningLog } : {}),
|
|
1563
|
+
...(tierLog ? { tierLog } : {}),
|
|
682
1564
|
};
|
|
683
1565
|
},
|
|
684
1566
|
|
|
685
|
-
async *parseStream(
|
|
1567
|
+
async *parseStream(
|
|
1568
|
+
response: Response,
|
|
1569
|
+
budget: TranslatorBudget,
|
|
1570
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1571
|
+
): AsyncGenerator<AdapterEvent> {
|
|
686
1572
|
if (!response.body) {
|
|
687
1573
|
yield { type: "error", message: "No response body" };
|
|
688
1574
|
return;
|
|
@@ -693,138 +1579,240 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
693
1579
|
const budgetEncoder = new TextEncoder();
|
|
694
1580
|
let buffer = "";
|
|
695
1581
|
let bufferBytes = 0;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1582
|
+
interface PendingToolCall {
|
|
1583
|
+
key: string;
|
|
1584
|
+
id: string;
|
|
1585
|
+
name: string;
|
|
1586
|
+
args: string;
|
|
1587
|
+
argsBytes: number;
|
|
1588
|
+
/**
|
|
1589
|
+
* Whether this call has ever received `arguments` as an actual string, empty included.
|
|
1590
|
+
* An empty string still counts: it proves the upstream sent the field with the right
|
|
1591
|
+
* wire type, which is what a later malformed repeat of that field would be padding for.
|
|
1592
|
+
* A canonical NAME is not evidence about the ARGUMENTS field and must not stand in.
|
|
1593
|
+
*/
|
|
1594
|
+
sawArgumentsString: boolean;
|
|
1595
|
+
}
|
|
705
1596
|
const pendingToolCalls: PendingToolCall[] = [];
|
|
706
1597
|
let toolCallSeq = 0;
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1598
|
+
const closeToolCalls = (): PendingToolCall[] => {
|
|
1599
|
+
const calls = [...pendingToolCalls];
|
|
1600
|
+
for (const call of calls) budget.closeCall(call.key);
|
|
1601
|
+
pendingToolCalls.length = 0;
|
|
1602
|
+
return calls;
|
|
1603
|
+
};
|
|
1604
|
+
const pendingToolCallsAreCompleteJsonObjects = (): boolean =>
|
|
1605
|
+
pendingToolCalls.length > 0 && pendingToolCalls.every(call => {
|
|
1606
|
+
if (call.name.trim().length === 0 || !call.sawArgumentsString || call.args.length === 0) return false;
|
|
1607
|
+
try {
|
|
1608
|
+
const parsed = JSON.parse(call.args) as unknown;
|
|
1609
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
1610
|
+
} catch {
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
// Returns "terminate" when a pending call cannot be dispatched, so every flush site
|
|
1615
|
+
// stops the turn instead of emitting an unusable call. `closeToolCalls()` runs first,
|
|
1616
|
+
// so budget reservations are released for every pending call even on the early return.
|
|
1617
|
+
const flushToolCalls = function* (): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
1618
|
+
for (const call of closeToolCalls()) {
|
|
1619
|
+
// Ingest already proved `name` is a string; the typeof guard keeps this branch
|
|
1620
|
+
// total so a future ingest change cannot turn a malformed name into a throw.
|
|
1621
|
+
if (typeof call.name !== "string" || call.name.trim().length === 0) {
|
|
1622
|
+
debugProviderDiagnostic("openai-chat", "tool-call-unnamed", {
|
|
1623
|
+
hadId: call.id.length > 0,
|
|
1624
|
+
argsBytes: call.argsBytes,
|
|
1625
|
+
});
|
|
1626
|
+
yield unnamedToolCallEvent(pendingUsage);
|
|
1627
|
+
return "terminate";
|
|
1628
|
+
}
|
|
711
1629
|
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
712
1630
|
yield { type: "tool_call_start", id: call.id, name: call.name };
|
|
713
1631
|
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
|
|
714
1632
|
yield { type: "tool_call_end" };
|
|
715
|
-
budget.closeCall(call.key);
|
|
716
1633
|
}
|
|
717
|
-
|
|
1634
|
+
return "continue";
|
|
1635
|
+
};
|
|
1636
|
+
const terminateWithError = function* (
|
|
1637
|
+
event: Extract<AdapterEvent, { type: "error" }>,
|
|
1638
|
+
): Generator<AdapterEvent, "terminate"> {
|
|
1639
|
+
closeToolCalls();
|
|
1640
|
+
yield event;
|
|
1641
|
+
return "terminate";
|
|
718
1642
|
};
|
|
719
1643
|
let pendingUsage: OcxUsage | undefined;
|
|
720
|
-
// Track terminal signals so a socket EOF without any terminator can fail closed instead of
|
|
721
|
-
// being reported as a clean completion (silent truncation). A graceful close is either an
|
|
722
|
-
// explicit `[DONE]` sentinel OR a chunk carrying a non-null `finish_reason` (some
|
|
723
|
-
// OpenAI-compatible providers omit `[DONE]` but do send finish_reason).
|
|
724
1644
|
let finishReason: string | undefined;
|
|
725
|
-
// Only answer text enables the finish-less EOF fallback. Reasoning-only streams can be
|
|
726
|
-
// suppressed by hideThinkingSummary and must not complete as empty successful turns.
|
|
727
1645
|
let sawUserFacingOutput = false;
|
|
728
1646
|
|
|
729
|
-
// Single per-line handler shared by the streaming loop and the EOF residual-frame flush, so
|
|
730
|
-
// a final frame is parsed identically wherever it lands (no duplicated, drift-prone parsing).
|
|
731
|
-
// Yields adapter events and returns "terminate" for a terminal frame ([DONE] / error) that
|
|
732
|
-
// must end the stream, or "continue" otherwise. Mutates the closure's terminal-signal state.
|
|
733
1647
|
const handleDataLine = function* (line: string): Generator<AdapterEvent, "continue" | "terminate"> {
|
|
734
|
-
|
|
735
|
-
|
|
1648
|
+
const rawPayload = sseFieldValue(line, "data");
|
|
1649
|
+
if (rawPayload === null) return "continue";
|
|
1650
|
+
const payload = rawPayload.trim();
|
|
1651
|
+
if (payload.length === 0) return "continue";
|
|
736
1652
|
if (payload === "[DONE]") {
|
|
737
|
-
yield* flushToolCalls();
|
|
738
|
-
const stopReason = finishReason
|
|
739
|
-
? "max_tokens"
|
|
740
|
-
: finishReason === "content_filter"
|
|
741
|
-
? "content_filter"
|
|
742
|
-
: undefined;
|
|
1653
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
1654
|
+
const stopReason = stopReasonFor(finishReason);
|
|
743
1655
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
744
1656
|
return "terminate";
|
|
745
1657
|
}
|
|
746
1658
|
|
|
747
|
-
let
|
|
1659
|
+
let parsed: unknown;
|
|
748
1660
|
try {
|
|
749
|
-
|
|
1661
|
+
parsed = JSON.parse(payload);
|
|
750
1662
|
} catch {
|
|
1663
|
+
tierMetadata?.markResponseUnparseable();
|
|
751
1664
|
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
752
1665
|
return "terminate";
|
|
753
1666
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (chunk.error) {
|
|
759
|
-
const err = chunk.error as { message?: string; code?: string; type?: string; status?: number } | undefined;
|
|
760
|
-
const message = err?.message ?? "upstream error";
|
|
761
|
-
debugProviderDiagnostic("openai-chat", "stream-error", { message });
|
|
762
|
-
yield* flushToolCalls();
|
|
763
|
-
yield {
|
|
764
|
-
type: "error",
|
|
765
|
-
message,
|
|
766
|
-
...(typeof err?.code === "string" ? { code: err.code } : {}),
|
|
767
|
-
...(typeof err?.type === "string" ? { errorType: err.type } : {}),
|
|
768
|
-
...(isCyberPolicyCode(err?.code)
|
|
769
|
-
? { status: 400 }
|
|
770
|
-
: typeof err?.status === "number" && Number.isInteger(err.status)
|
|
771
|
-
? { status: err.status }
|
|
772
|
-
: {}),
|
|
773
|
-
};
|
|
774
|
-
return "terminate";
|
|
1667
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "continue";
|
|
1668
|
+
const chunk = parsed as Record<string, unknown>;
|
|
1669
|
+
if (Object.hasOwn(chunk, "service_tier")) {
|
|
1670
|
+
tierMetadata?.observeResponseServiceTier(chunk.service_tier);
|
|
775
1671
|
}
|
|
776
1672
|
|
|
777
|
-
if (chunk.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
|
|
1673
|
+
if (chunk.error !== undefined && chunk.error !== null) {
|
|
1674
|
+
const event = upstreamErrorEvent(chunk.error, pendingUsage);
|
|
1675
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
1676
|
+
return yield* terminateWithError(event);
|
|
782
1677
|
}
|
|
783
1678
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
1679
|
+
if (chunk.usage) pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
|
|
1680
|
+
|
|
1681
|
+
const choices = chunk.choices;
|
|
1682
|
+
if (choices === undefined) return "continue";
|
|
1683
|
+
if (!Array.isArray(choices)) return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
1684
|
+
if (choices.length === 0) return "continue";
|
|
1685
|
+
const rawChoice = choices[0];
|
|
1686
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
1687
|
+
return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
|
|
1688
|
+
}
|
|
1689
|
+
const choice = rawChoice as {
|
|
1690
|
+
delta?: Record<string, unknown>;
|
|
1691
|
+
finish_reason?: string;
|
|
1692
|
+
error?: unknown;
|
|
1693
|
+
};
|
|
1694
|
+
if (choice.finish_reason === "error") {
|
|
1695
|
+
const event = upstreamErrorEvent(choice.error, pendingUsage);
|
|
1696
|
+
debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
|
|
1697
|
+
return yield* terminateWithError(event);
|
|
790
1698
|
}
|
|
791
|
-
|
|
1699
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) finishReason = choice.finish_reason;
|
|
1700
|
+
const delta = choice.delta;
|
|
792
1701
|
if (delta) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
1702
|
+
const reasoningText = reasoningTextFrom(delta);
|
|
1703
|
+
if (reasoningText !== undefined) yield { type: "reasoning_raw_delta", text: reasoningText };
|
|
796
1704
|
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
797
1705
|
sawUserFacingOutput = true;
|
|
798
1706
|
yield { type: "text_delta", text: delta.content };
|
|
799
1707
|
}
|
|
800
1708
|
|
|
801
|
-
const
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1709
|
+
const rawToolCalls = delta.tool_calls;
|
|
1710
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
1711
|
+
// A non-null claimed tool-call payload is not benign padding. Dropping it can leave the
|
|
1712
|
+
// matching result permanently orphaned, so malformed nested shapes fail closed
|
|
1713
|
+
// through the adapter error channel instead of escaping as TypeError (#1325). Null is
|
|
1714
|
+
// tolerated as absent because OpenAI-compatible providers may emit it as stream padding.
|
|
1715
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
1716
|
+
logInvalidToolCalls("stream", rawToolCalls);
|
|
1717
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage));
|
|
1718
|
+
}
|
|
1719
|
+
for (let callIndex = 0; callIndex < rawToolCalls.length; callIndex++) {
|
|
1720
|
+
const rawToolCall: unknown = rawToolCalls[callIndex];
|
|
1721
|
+
if (!isRecord(rawToolCall)) {
|
|
1722
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1723
|
+
reason: "tool_call_not_object",
|
|
1724
|
+
callIndex,
|
|
1725
|
+
valueType: rawToolCall === null ? "null" : Array.isArray(rawToolCall) ? "array" : typeof rawToolCall,
|
|
1726
|
+
};
|
|
1727
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1728
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1729
|
+
}
|
|
1730
|
+
// This is upstream JSON, so every field is validated before it is stored: a
|
|
1731
|
+
// malformed value must fail closed through the #1325 channel here rather than
|
|
1732
|
+
// escaping later as a TypeError from string handling at flush time.
|
|
1733
|
+
const rawFunction = rawToolCall.function;
|
|
1734
|
+
if (rawFunction !== undefined && rawFunction !== null && !isRecord(rawFunction)) {
|
|
1735
|
+
const diagnostic: InvalidToolCallDiagnostic = {
|
|
1736
|
+
reason: "tool_call_function_not_object",
|
|
1737
|
+
callIndex,
|
|
1738
|
+
valueType: Array.isArray(rawFunction) ? "array" : typeof rawFunction,
|
|
1739
|
+
};
|
|
1740
|
+
logInvalidToolCalls("stream", rawToolCalls, diagnostic);
|
|
1741
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, diagnostic));
|
|
1742
|
+
}
|
|
1743
|
+
const fnRecord = isRecord(rawFunction) ? rawFunction : undefined;
|
|
1744
|
+
const rawName = fnRecord?.name;
|
|
1745
|
+
const rawArguments = fnRecord?.arguments;
|
|
1746
|
+
const rawId = rawToolCall.id;
|
|
1747
|
+
const idDelta = typeof rawId === "string" ? rawId : "";
|
|
1748
|
+
const rawIndex = rawToolCall.index;
|
|
1749
|
+
|
|
1750
|
+
// Resolve the pending call BEFORE judging the fields. Some OpenAI-compatible
|
|
1751
|
+
// streamers repeat an already-sent field as a non-string placeholder on a
|
|
1752
|
+
// continuation delta; judging first meant the whole stream died with a 502 even
|
|
1753
|
+
// though the value being repeated was already held in canonical form.
|
|
1754
|
+
const key = typeof rawIndex === "number"
|
|
1755
|
+
? `i:${rawIndex}`
|
|
1756
|
+
: idDelta
|
|
1757
|
+
? `id:${idDelta}`
|
|
1758
|
+
: pendingToolCalls[pendingToolCalls.length - 1]?.key;
|
|
809
1759
|
let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
|
|
810
|
-
|
|
811
|
-
// id-only continuation for the same provider id (and vice versa) instead of
|
|
812
|
-
// splitting into two calls that share one call_id downstream.
|
|
813
|
-
if (!call && tc.id) call = pendingToolCalls.find(c => c.id === tc.id);
|
|
1760
|
+
if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta);
|
|
814
1761
|
if (!call) {
|
|
815
|
-
call = {
|
|
1762
|
+
call = {
|
|
1763
|
+
key: key ?? `seq:${pendingToolCalls.length}`,
|
|
1764
|
+
id: "",
|
|
1765
|
+
name: "",
|
|
1766
|
+
args: "",
|
|
1767
|
+
argsBytes: 0,
|
|
1768
|
+
sawArgumentsString: false,
|
|
1769
|
+
};
|
|
816
1770
|
pendingToolCalls.push(call);
|
|
817
1771
|
budget.openCall(call.key);
|
|
818
1772
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1773
|
+
|
|
1774
|
+
// Tolerance is per FIELD, keyed on that field's own provenance. A canonical name
|
|
1775
|
+
// says nothing about whether `arguments` was ever sent as a string, so it cannot
|
|
1776
|
+
// authorize a malformed arguments value — that would silently drop a real
|
|
1777
|
+
// argument payload the model intended to send.
|
|
1778
|
+
const rejection: InvalidToolCallDiagnostic | undefined =
|
|
1779
|
+
isInvalidStreamStringField(rawName) && call.name.trim() === ""
|
|
1780
|
+
? { reason: "tool_call_function_name_invalid", callIndex, valueType: typeof rawName }
|
|
1781
|
+
: isInvalidStreamStringField(rawArguments) && !call.sawArgumentsString
|
|
1782
|
+
? { reason: "tool_call_function_arguments_invalid", callIndex, valueType: typeof rawArguments }
|
|
1783
|
+
: isInvalidStreamStringField(rawId) && call.id === ""
|
|
1784
|
+
? { reason: "tool_call_id_invalid", callIndex, valueType: typeof rawId }
|
|
1785
|
+
: undefined;
|
|
1786
|
+
if (rejection) {
|
|
1787
|
+
logInvalidToolCalls("stream", rawToolCalls, rejection);
|
|
1788
|
+
return yield* terminateWithError(invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage, rejection));
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (idDelta && !call.id) call.id = idDelta;
|
|
1792
|
+
if (typeof rawName === "string" && rawName && !call.name) call.name = rawName;
|
|
1793
|
+
if (typeof rawArguments === "string") call.sawArgumentsString = true;
|
|
1794
|
+
// Tool-call deltas are BUFFERED until a terminal signal, so this adapter can
|
|
1795
|
+
// consume upstream frames for a long time while yielding nothing. The Responses
|
|
1796
|
+
// bridge reads adapter activity, not socket activity, so a model that streams a
|
|
1797
|
+
// large argument payload looks identical to a hung upstream and the stall
|
|
1798
|
+
// watchdog can abort a turn that was progressing normally.
|
|
1799
|
+
//
|
|
1800
|
+
// Found while investigating #2156, but it is NOT that bug: a stall abort emits
|
|
1801
|
+
// `response.incomplete` with `upstream_stall_timeout` from the bridge, whereas
|
|
1802
|
+
// that report shows the adapter's own end-of-stream error after `reader.read()`
|
|
1803
|
+
// returned EOF with tool calls still pending. Different path, different frame.
|
|
1804
|
+
//
|
|
1805
|
+
// A heartbeat is invisible downstream — the bridge consumes it to re-arm the
|
|
1806
|
+
// watchdog and emits nothing — which is the same remedy the Cursor, Anthropic,
|
|
1807
|
+
// Google, and Kiro adapters already use for their own silent phases.
|
|
1808
|
+
yield { type: "heartbeat" };
|
|
1809
|
+
if (typeof rawArguments === "string" && rawArguments) {
|
|
822
1810
|
const previousBytes = call.argsBytes;
|
|
823
|
-
const nextBytes = previousBytes + budgetEncoder.encode(
|
|
1811
|
+
const nextBytes = previousBytes + budgetEncoder.encode(rawArguments).byteLength;
|
|
824
1812
|
const scope = { kind: "tool_args" as const, callId: call.key };
|
|
825
1813
|
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
826
1814
|
try {
|
|
827
|
-
call.args +=
|
|
1815
|
+
call.args += rawArguments;
|
|
828
1816
|
reservation.commitRetained();
|
|
829
1817
|
budget.releaseRetained(previousBytes, scope);
|
|
830
1818
|
call.argsBytes = nextBytes;
|
|
@@ -837,10 +1825,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
837
1825
|
}
|
|
838
1826
|
}
|
|
839
1827
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
|
|
843
|
-
yield* flushToolCalls();
|
|
1828
|
+
if (typeof choice.finish_reason === "string" && choice.finish_reason) {
|
|
1829
|
+
if ((yield* flushToolCalls()) === "terminate") return "terminate";
|
|
844
1830
|
}
|
|
845
1831
|
return "continue";
|
|
846
1832
|
};
|
|
@@ -880,23 +1866,20 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
880
1866
|
}
|
|
881
1867
|
}
|
|
882
1868
|
|
|
883
|
-
// Some providers send the terminal `data:` frame (carrying the final delta, finish_reason,
|
|
884
|
-
// and/or usage) WITHOUT a trailing newline before closing the socket, so it never crosses
|
|
885
|
-
// the split("\n") boundary and stays in `buffer`. Run it through the SAME handler so its
|
|
886
|
-
// content/tool-calls are emitted and its terminal signal observed — otherwise a genuinely
|
|
887
|
-
// complete stream loses its last frame and may be falsely failed below.
|
|
888
1869
|
if (buffer.length > 0) {
|
|
889
1870
|
if ((yield* handleDataLine(buffer)) === "terminate") return;
|
|
890
1871
|
}
|
|
891
|
-
// Reader EOF. Prefer failing closed before flushing pending tool calls so the bridge
|
|
892
|
-
// never sees a fabricated tool_call_end on a truncated mid-assembly stream.
|
|
893
|
-
//
|
|
894
|
-
// Checked BEFORE flushToolCalls(), because that helper emits tool_call_end and there is no
|
|
895
|
-
// taking it back: a half-assembled argument string would reach the client as a completed
|
|
896
|
-
// call. Tool calls are buffered here (unlike the Anthropic adapter, which forwards
|
|
897
|
-
// fragments live), so this adapter can still decide.
|
|
898
1872
|
const sawFinish = finishReason !== undefined;
|
|
899
1873
|
if (!sawFinish && pendingToolCalls.length > 0) {
|
|
1874
|
+
// Some OpenAI-compatible gateways close immediately after a complete function-call
|
|
1875
|
+
// delta and omit both terminal conventions. Keep the default fail-closed policy, and
|
|
1876
|
+
// let an opted-in provider recover only calls whose assembled argument payload is a
|
|
1877
|
+
// complete JSON object. A partial JSON prefix still takes the truncation path below.
|
|
1878
|
+
if (provider.openaiChatEofTolerance === true && pendingToolCallsAreCompleteJsonObjects()) {
|
|
1879
|
+
if ((yield* flushToolCalls()) === "terminate") return;
|
|
1880
|
+
yield { type: "done", usage: pendingUsage };
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
900
1883
|
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
901
1884
|
finishReason: null,
|
|
902
1885
|
hadUsage: pendingUsage !== undefined,
|
|
@@ -905,9 +1888,6 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
905
1888
|
yield { type: "error", message: "upstream stream ended mid tool call without a terminal signal — possible truncation" };
|
|
906
1889
|
return;
|
|
907
1890
|
}
|
|
908
|
-
// Finish-less EOF is only safe when answer text was emitted. Reasoning-only / usage-only
|
|
909
|
-
// truncations must stay on the error path (hideThinkingSummary can suppress reasoning).
|
|
910
|
-
// Trailing usage alone is not a terminal signal for this adapter (#735 / restore #773).
|
|
911
1891
|
if (!sawFinish && !sawUserFacingOutput) {
|
|
912
1892
|
debugProviderDiagnostic("openai-chat", "stream-truncated", {
|
|
913
1893
|
finishReason: finishReason ?? null,
|
|
@@ -916,13 +1896,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
916
1896
|
yield { type: "error", message: "upstream stream ended without a terminal signal ([DONE] or finish_reason) — possible truncation" };
|
|
917
1897
|
return;
|
|
918
1898
|
}
|
|
919
|
-
yield* flushToolCalls();
|
|
920
|
-
|
|
921
|
-
const stopReason = finishReason === "length"
|
|
922
|
-
? "max_tokens"
|
|
923
|
-
: finishReason === "content_filter"
|
|
924
|
-
? "content_filter"
|
|
925
|
-
: undefined;
|
|
1899
|
+
if ((yield* flushToolCalls()) === "terminate") return;
|
|
1900
|
+
const stopReason = stopReasonFor(finishReason);
|
|
926
1901
|
yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
|
|
927
1902
|
} catch (error) {
|
|
928
1903
|
if (isTranslatorBudgetExceededError(error)
|
|
@@ -940,63 +1915,121 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
940
1915
|
throw error;
|
|
941
1916
|
} finally {
|
|
942
1917
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
943
|
-
|
|
1918
|
+
closeToolCalls();
|
|
944
1919
|
reader.releaseLock();
|
|
945
1920
|
}
|
|
946
1921
|
},
|
|
947
1922
|
|
|
948
|
-
async parseResponse(
|
|
949
|
-
|
|
1923
|
+
async parseResponse(
|
|
1924
|
+
response: Response,
|
|
1925
|
+
budget: TranslatorBudget,
|
|
1926
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1927
|
+
): Promise<AdapterEvent[]> {
|
|
1928
|
+
let parsed: unknown;
|
|
1929
|
+
try {
|
|
1930
|
+
parsed = await response.json();
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
tierMetadata?.markResponseUnparseable();
|
|
1933
|
+
throw error;
|
|
1934
|
+
}
|
|
1935
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1936
|
+
tierMetadata?.markResponseUnparseable();
|
|
1937
|
+
throw new Error("upstream response was not a JSON object");
|
|
1938
|
+
}
|
|
1939
|
+
const json = parsed as Record<string, unknown>;
|
|
1940
|
+
if (Object.hasOwn(json, "service_tier")) {
|
|
1941
|
+
tierMetadata?.observeResponseServiceTier(json.service_tier);
|
|
1942
|
+
}
|
|
950
1943
|
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
951
1944
|
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
952
1945
|
try {
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return [{
|
|
964
|
-
type: "error",
|
|
965
|
-
message,
|
|
966
|
-
...(code !== undefined ? { code } : {}),
|
|
967
|
-
...(errorType !== undefined ? { errorType } : {}),
|
|
968
|
-
...(status !== undefined ? { status } : {}),
|
|
969
|
-
}];
|
|
970
|
-
}
|
|
1946
|
+
const payload = unwrapChatCompletionPayload(json);
|
|
1947
|
+
const usage = usageFromOpenAIChat(payload.usage as Record<string, unknown> | undefined);
|
|
1948
|
+
if (json.success === false && payload.error === undefined) {
|
|
1949
|
+
return [{
|
|
1950
|
+
type: "error",
|
|
1951
|
+
message: "upstream reported failure without an error payload",
|
|
1952
|
+
...(usage ? { usage } : {}),
|
|
1953
|
+
}];
|
|
1954
|
+
}
|
|
1955
|
+
if (payload.error !== undefined && payload.error !== null) return [upstreamErrorEvent(payload.error, usage)];
|
|
971
1956
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1957
|
+
const events: AdapterEvent[] = [];
|
|
1958
|
+
const choices = payload.choices as {
|
|
1959
|
+
message?: Record<string, unknown>;
|
|
1960
|
+
finish_reason?: unknown;
|
|
1961
|
+
error?: OpenAIChatError;
|
|
1962
|
+
}[] | undefined;
|
|
1963
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
1964
|
+
return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
1965
|
+
}
|
|
1966
|
+
const rawChoice = choices[0];
|
|
1967
|
+
if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
|
|
1968
|
+
return [invalidChoicesEvent(usage)];
|
|
1969
|
+
}
|
|
1970
|
+
const choice = rawChoice;
|
|
1971
|
+
if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
|
|
1972
|
+
if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
|
|
1973
|
+
// `!choice.message` splits this input class on TRUTHINESS, not on shape: `null` and `0` fail
|
|
1974
|
+
// closed here, while `"text"`, `true` and `[{...}]` pass and every property read below yields
|
|
1975
|
+
// `undefined` — so a choice claiming an assistant message completed as a SUCCESSFUL EMPTY
|
|
1976
|
+
// turn, stranding any tool call it claimed. The one line above already validates the choice
|
|
1977
|
+
// container this way; its message was left on a truthiness test.
|
|
1978
|
+
//
|
|
1979
|
+
// Every non-record is rejected, arrays included. The google adapter does carve out `[]` for
|
|
1980
|
+
// `content`, but that carve-out is specific to a protobuf-derived wire where a repeated
|
|
1981
|
+
// field can spell an empty message — and `content` is genuinely an ARRAY of blocks there.
|
|
1982
|
+
// `message` is a record on a plain-JSON wire that already has `{}`, so importing the
|
|
1983
|
+
// exception would be an analogy rather than evidence. `[{"content":"…"}]` is the case that
|
|
1984
|
+
// matters: it discards a complete answer, #2232's `content: [{ parts: [...] }]` one adapter over.
|
|
1985
|
+
//
|
|
1986
|
+
// Read through `unknown` rather than the declared type: `choices` is a cast over wire data,
|
|
1987
|
+
// so its `message?: Record<string, unknown>` is an assertion the upstream never made, and
|
|
1988
|
+
// narrowing against it is what let the missing check look type-safe.
|
|
1989
|
+
const rawMessage: unknown = choice.message;
|
|
1990
|
+
if (!isRecord(rawMessage)) {
|
|
1991
|
+
return [invalidChoicesEvent(usage)];
|
|
1992
|
+
}
|
|
977
1993
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
events.push({ type: "reasoning_raw_delta", text:
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1994
|
+
const msg = rawMessage as Record<string, unknown>;
|
|
1995
|
+
const reasoningText = reasoningTextFrom(msg);
|
|
1996
|
+
if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
|
|
1997
|
+
if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });
|
|
1998
|
+
const rawToolCalls = msg.tool_calls;
|
|
1999
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
2000
|
+
if (!Array.isArray(rawToolCalls)) {
|
|
2001
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
2002
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
2003
|
+
}
|
|
2004
|
+
for (const rawToolCall of rawToolCalls) {
|
|
2005
|
+
if (!isRecord(rawToolCall) || !isRecord(rawToolCall.function)) {
|
|
2006
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
2007
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
2008
|
+
}
|
|
2009
|
+
const id = rawToolCall.id;
|
|
2010
|
+
const name = rawToolCall.function.name;
|
|
2011
|
+
const args = rawToolCall.function.arguments;
|
|
2012
|
+
// A blank name is as undispatchable as a missing one, so it fails closed here
|
|
2013
|
+
// for the same reason the streamed path refuses it. Trimmed length, not `!name`:
|
|
2014
|
+
// a whitespace-only function name is not a legitimate tool-call shape either.
|
|
2015
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof args !== "string"
|
|
2016
|
+
|| name.trim().length === 0) {
|
|
2017
|
+
logInvalidToolCalls("response", rawToolCalls);
|
|
2018
|
+
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
2019
|
+
}
|
|
2020
|
+
events.push({ type: "tool_call_start", id, name });
|
|
2021
|
+
events.push({ type: "tool_call_delta", arguments: args });
|
|
2022
|
+
events.push({ type: "tool_call_end" });
|
|
2023
|
+
}
|
|
991
2024
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
2025
|
+
const stopReason = stopReasonFor(choice.finish_reason);
|
|
2026
|
+
events.push({
|
|
2027
|
+
type: "done",
|
|
2028
|
+
usage,
|
|
2029
|
+
...(stopReason ? { stopReason } : {}),
|
|
2030
|
+
});
|
|
2031
|
+
retainTranslatedEventBatch(events, budget);
|
|
2032
|
+
return events;
|
|
1000
2033
|
} finally {
|
|
1001
2034
|
budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
|
|
1002
2035
|
}
|