@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,14 +1,34 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
|
-
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
|
|
3
|
+
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
|
|
4
4
|
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
|
|
5
|
-
import { COMPACT_PROMPT, decodeCompactionSummary,
|
|
5
|
+
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../responses/compaction";
|
|
6
6
|
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
7
|
+
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
|
|
7
8
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
8
|
-
import {
|
|
9
|
+
import { debugProviderDiagnostic } from "../lib/debug";
|
|
10
|
+
import {
|
|
11
|
+
CODEX_FORWARD_BASE_URL,
|
|
12
|
+
destinationDecodesNativeCompactionBlob,
|
|
13
|
+
isCanonicalOpenAiForwardProvider,
|
|
14
|
+
isOpenAiOperatedResponsesDestination,
|
|
15
|
+
} from "../providers/openai-tiers";
|
|
9
16
|
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
10
17
|
import { modelRecordValue } from "../reasoning-effort";
|
|
11
18
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
19
|
+
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
|
|
20
|
+
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
21
|
+
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
|
|
22
|
+
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
23
|
+
import { normalizeXaiResponsesWebSearch } from "./xai-web-search";
|
|
24
|
+
import {
|
|
25
|
+
isXaiSchemaTarget,
|
|
26
|
+
normalizeXaiToolParameters,
|
|
27
|
+
XaiToolSchemaCompatibilityError,
|
|
28
|
+
} from "./xai-tool-schema";
|
|
29
|
+
import {
|
|
30
|
+
createAdapterTierMetadata,
|
|
31
|
+
} from "../providers/fastwire";
|
|
12
32
|
|
|
13
33
|
// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
|
|
14
34
|
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
|
|
@@ -32,7 +52,23 @@ export const FORWARD_HEADERS = [
|
|
|
32
52
|
"x-responsesapi-include-timing-metrics",
|
|
33
53
|
];
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
|
|
57
|
+
* native `encrypted_content` guarantees only that blob value: `status` is always removed;
|
|
58
|
+
* proxy-owned `ocxr1:` envelopes are always removed; and native blobs are removed when the caller
|
|
59
|
+
* requests stripping after a route-identity change or opaque-blob recovery. On routed/non-OpenAI
|
|
60
|
+
* destinations, a present non-array `content` field is omitted. Otherwise non-empty array content
|
|
61
|
+
* is blanked unless raw reasoning preservation is enabled; removing an `ocxr1:` envelope selects
|
|
62
|
+
* the same blanking path when non-array omission is not active.
|
|
63
|
+
*/
|
|
64
|
+
export function sanitizeReasoningInputContent(
|
|
65
|
+
body: unknown,
|
|
66
|
+
opts?: {
|
|
67
|
+
preserveRawReasoningContent?: boolean;
|
|
68
|
+
dropNullContentChannel?: boolean;
|
|
69
|
+
stripEncryptedContent?: boolean;
|
|
70
|
+
},
|
|
71
|
+
): unknown {
|
|
36
72
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
37
73
|
const raw = body as Record<string, unknown>;
|
|
38
74
|
if (!Array.isArray(raw.input)) return body;
|
|
@@ -46,13 +82,47 @@ export function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
|
46
82
|
// ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
|
|
47
83
|
// backend cannot decrypt them and would reject the request. Strip regardless of content shape.
|
|
48
84
|
const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
|
|
49
|
-
|
|
85
|
+
const hasOutputStatus = Object.prototype.hasOwnProperty.call(rec, "status");
|
|
86
|
+
const hasEncryptedContent = Object.prototype.hasOwnProperty.call(rec, "encrypted_content");
|
|
87
|
+
const stripEncryptedContent = hasOcxEnvelope
|
|
88
|
+
|| (opts?.stripEncryptedContent === true && hasEncryptedContent);
|
|
89
|
+
// Codex serializes an absent reasoning content channel as `"content": null`. The field is
|
|
90
|
+
// optional and null carries nothing, but a strict gateway rejects the item on its declared type
|
|
91
|
+
// — xAI answers `Could not decode the compaction blob`, naming the sibling `encrypted_content`
|
|
92
|
+
// rather than the field it actually refused, which is why this reads as a blob failure. Drop the
|
|
93
|
+
// key so the item matches the shape the upstream issued.
|
|
94
|
+
//
|
|
95
|
+
// Gated to routed destinations. An OpenAI-operated backend rejects a blob-bearing item when its
|
|
96
|
+
// null `content` channel is deleted (`The encrypted content ... could not be verified`); that
|
|
97
|
+
// live result establishes this channel constraint, not whole-item shape preservation. The gate
|
|
98
|
+
// is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its own
|
|
99
|
+
// blob without the null channel. This is independent of the output-only status removal below.
|
|
100
|
+
const dropNullContentChannel = opts?.dropNullContentChannel === true
|
|
101
|
+
&& "content" in rec && !Array.isArray(rec.content);
|
|
102
|
+
// `status` is output-only. Measured OpenAI reasoning items never contain it, and Grok accepts
|
|
103
|
+
// its own encrypted_content with status removed. Keeping a foreign status beside a retained
|
|
104
|
+
// blob makes OpenAI reject the field before blob validation, starving the provenance recovery
|
|
105
|
+
// of the opaque-blob error it needs. Content blanking remains the separate pre-existing rule.
|
|
106
|
+
const stripOutputStatus = hasOutputStatus;
|
|
107
|
+
const blankContent = !dropNullContentChannel
|
|
108
|
+
&& !opts?.preserveRawReasoningContent
|
|
109
|
+
&& (hasRawContent || hasOcxEnvelope);
|
|
110
|
+
if (!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel) {
|
|
111
|
+
return item;
|
|
112
|
+
}
|
|
50
113
|
changed = true;
|
|
114
|
+
const next: Record<string, unknown> = { ...rec };
|
|
115
|
+
if (dropNullContentChannel) delete next.content;
|
|
116
|
+
if (stripOutputStatus) delete next.status;
|
|
117
|
+
if (stripEncryptedContent) delete next.encrypted_content;
|
|
51
118
|
// Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
|
|
52
119
|
// native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
|
|
53
120
|
// `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
|
|
54
|
-
|
|
55
|
-
|
|
121
|
+
// DeepSeek's Responses API instead ACCEPTS plaintext reasoning replay (its compatibility
|
|
122
|
+
// guide merges reasoning items into the adjacent assistant message), so providers flagged
|
|
123
|
+
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
124
|
+
// continuations after tool calls (issue #875 family).
|
|
125
|
+
if (blankContent) next.content = [];
|
|
56
126
|
return next;
|
|
57
127
|
});
|
|
58
128
|
|
|
@@ -100,6 +170,71 @@ function stripInvalidItemIds(body: unknown): unknown {
|
|
|
100
170
|
return changed ? { ...body, input } : body;
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Codex-private tool fields that only the ChatGPT backend understands.
|
|
175
|
+
*
|
|
176
|
+
* A third-party Responses gateway validates its schema and rejects the whole request before
|
|
177
|
+
* inference — xAI answers `Argument not supported: external_web_access` — so these are removed at
|
|
178
|
+
* the noncanonical boundary while the tool and every public option stay.
|
|
179
|
+
*
|
|
180
|
+
* Keep this a table. Each private bit Codex attaches has so far arrived as its own bespoke strip
|
|
181
|
+
* with its own traversal, and the traversals disagreed about which containers they covered; a new
|
|
182
|
+
* one should be a row here instead. `toolTypes` omitted means the field is private on any tool.
|
|
183
|
+
*/
|
|
184
|
+
const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: ReadonlySet<string>; capabilityGated?: boolean }[] = [
|
|
185
|
+
// ChatGPT's browsing policy bit. The public hosted tool is enabled by its presence alone.
|
|
186
|
+
// OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
|
|
187
|
+
// it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
|
|
188
|
+
// === false), matching stripOpenAiOnlyWebSearchFields; see
|
|
189
|
+
// tests/responses-routed-web-search-fields.test.ts.
|
|
190
|
+
{ field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
|
|
191
|
+
// Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
|
|
192
|
+
// already loaded, so a still-deferred declaration — including one promoted out of a namespace
|
|
193
|
+
// group — otherwise reaches the wire carrying it.
|
|
194
|
+
{ field: "defer_loading" },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
function stripCanonicalOnlyToolFields(body: unknown, includeCapabilityGated: boolean): unknown {
|
|
198
|
+
if (!isPlainObject(body)) return body;
|
|
199
|
+
|
|
200
|
+
const rewriteTools = (tools: unknown[]): unknown[] => {
|
|
201
|
+
let changed = false;
|
|
202
|
+
const rewritten = tools.map(tool => {
|
|
203
|
+
if (!isPlainObject(tool)) return tool;
|
|
204
|
+
let next = tool;
|
|
205
|
+
for (const { field, toolTypes, capabilityGated } of CANONICAL_ONLY_TOOL_FIELDS) {
|
|
206
|
+
if (capabilityGated && !includeCapabilityGated) continue;
|
|
207
|
+
if (!Object.hasOwn(next, field)) continue;
|
|
208
|
+
if (toolTypes && (typeof next.type !== "string" || !toolTypes.has(next.type))) continue;
|
|
209
|
+
const { [field]: _private, ...rest } = next;
|
|
210
|
+
next = rest;
|
|
211
|
+
}
|
|
212
|
+
if (next === tool) return tool;
|
|
213
|
+
changed = true;
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
return changed ? rewritten : tools;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
let rewrittenBody = body;
|
|
220
|
+
if (Array.isArray(body.tools)) {
|
|
221
|
+
const tools = rewriteTools(body.tools);
|
|
222
|
+
if (tools !== body.tools) rewrittenBody = { ...rewrittenBody, tools };
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(body.input)) return rewrittenBody;
|
|
225
|
+
|
|
226
|
+
let input: unknown[] | undefined;
|
|
227
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
228
|
+
const item = body.input[index];
|
|
229
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) continue;
|
|
230
|
+
const tools = rewriteTools(item.tools);
|
|
231
|
+
if (tools === item.tools) continue;
|
|
232
|
+
input ??= [...body.input];
|
|
233
|
+
input[index] = { ...item, tools };
|
|
234
|
+
}
|
|
235
|
+
return input ? { ...rewrittenBody, input } : rewrittenBody;
|
|
236
|
+
}
|
|
237
|
+
|
|
103
238
|
/**
|
|
104
239
|
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
105
240
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
@@ -123,43 +258,47 @@ function stripItemIdsWhenUnstored(body: unknown): unknown {
|
|
|
123
258
|
}
|
|
124
259
|
|
|
125
260
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
261
|
+
* Normalize replayed compaction items for the destination backend.
|
|
262
|
+
*
|
|
263
|
+
* A compaction item carries an `encrypted_content` blob the client replays verbatim on every later
|
|
264
|
+
* turn, and only the backend that minted it can decode it. Proxy-minted `ocx1:` envelopes are
|
|
265
|
+
* transparent base64 rather than encryption, so no upstream can read them and they always become
|
|
266
|
+
* plain user messages. Native blobs have multiple possible minters, so a destination's ability to
|
|
267
|
+
* decode its own blobs does not make a blob from a previous serving identity portable. On a known
|
|
268
|
+
* identity mismatch the blob degrades to the same note the bridged parser uses, even when the
|
|
269
|
+
* destination normally accepts native blobs. Without a known mismatch, the destination capability
|
|
270
|
+
* keeps the existing behavior.
|
|
271
|
+
*
|
|
272
|
+
* A bare `context_compaction` marker carries no blob and is forwarded untouched.
|
|
130
273
|
*/
|
|
131
|
-
function scrubOcxCompactionItems(
|
|
274
|
+
function scrubOcxCompactionItems(
|
|
275
|
+
body: unknown,
|
|
276
|
+
destinationDecodesNativeBlob: boolean,
|
|
277
|
+
threadServingIdentityChanged: boolean,
|
|
278
|
+
): unknown {
|
|
132
279
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
133
280
|
|
|
134
281
|
let changed = false;
|
|
135
282
|
const input = body.input.map(item => {
|
|
136
|
-
if (!isPlainObject(item)) return item;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
283
|
+
if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
|
|
284
|
+
const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
|
|
285
|
+
if (encrypted === undefined) return item;
|
|
286
|
+
if (
|
|
287
|
+
decodeCompactionSummary(encrypted) === null
|
|
288
|
+
&& destinationDecodesNativeBlob
|
|
289
|
+
&& !threadServingIdentityChanged
|
|
290
|
+
) return item;
|
|
140
291
|
changed = true;
|
|
141
292
|
return {
|
|
142
293
|
type: "message",
|
|
143
294
|
role: "user",
|
|
144
|
-
content: [{ type: "input_text", text:
|
|
295
|
+
content: [{ type: "input_text", text: compactionItemToText(encrypted) }],
|
|
145
296
|
};
|
|
146
297
|
});
|
|
147
298
|
|
|
148
299
|
return changed ? { ...body, input } : body;
|
|
149
300
|
}
|
|
150
301
|
|
|
151
|
-
/**
|
|
152
|
-
* Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
|
|
153
|
-
* attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
|
|
154
|
-
* model, and the passthrough path forwards the raw body untouched — so a slug that doesn't support
|
|
155
|
-
* the tool 400s (`Tool 'image_generation' is not supported with gpt-5.3-codex-spark.`). Each entry
|
|
156
|
-
* maps a model-slug matcher to the hosted tool types that must be stripped before forwarding.
|
|
157
|
-
* Extend this when another native slug rejects a hosted tool (e.g. `code_interpreter`).
|
|
158
|
-
*/
|
|
159
|
-
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
|
|
160
|
-
{ match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
|
|
161
|
-
];
|
|
162
|
-
|
|
163
302
|
/**
|
|
164
303
|
* Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
|
|
165
304
|
* codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
|
|
@@ -178,6 +317,40 @@ function stripUnsupportedReasoningParams(body: unknown): unknown {
|
|
|
178
317
|
return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
|
|
179
318
|
}
|
|
180
319
|
|
|
320
|
+
/**
|
|
321
|
+
* GPT-5.6 retired the legacy 24-hour retention field, and the ChatGPT backend 400s the whole
|
|
322
|
+
* request when that field is present (issue #2092).
|
|
323
|
+
*
|
|
324
|
+
* The retired field is NOT translated to the replacement: 5.6 carries a different TTL contract,
|
|
325
|
+
* and implicit caching still applies when the caller sent no replacement options. Inventing a
|
|
326
|
+
* value here would silently change a caching decision the caller never made.
|
|
327
|
+
*
|
|
328
|
+
* Deliberately narrow on both axes, because a wider strip is a behavior change rather than a fix:
|
|
329
|
+
* only the gpt-5.6 family (an older model may still honor the field), and only on the canonical
|
|
330
|
+
* ChatGPT backend, which is the deployment that rejects it. Matching is exact-or-dashed-prefix so
|
|
331
|
+
* a future `gpt-5.60` is not swept up by a bare `startsWith`.
|
|
332
|
+
*/
|
|
333
|
+
function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): unknown {
|
|
334
|
+
if (!isPlainObject(body)) return body;
|
|
335
|
+
if (typeof modelId !== "string") return body;
|
|
336
|
+
if (modelId !== "gpt-5.6" && !modelId.startsWith("gpt-5.6-")) return body;
|
|
337
|
+
if (!Object.hasOwn(body, "prompt_cache_retention")) return body;
|
|
338
|
+
const { prompt_cache_retention: _retention, ...rest } = body;
|
|
339
|
+
return rest;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Public Responses clients can send `prompt_cache_options`, but the canonical ChatGPT Codex
|
|
344
|
+
* backend rejects the top-level field before inference (issue #2765). Custom forward gateways and
|
|
345
|
+
* API-key Responses providers own different wire contracts, so the caller applies this only after
|
|
346
|
+
* the canonical destination predicate succeeds.
|
|
347
|
+
*/
|
|
348
|
+
function stripCanonicalForwardPromptCacheOptions(body: unknown): unknown {
|
|
349
|
+
if (!isPlainObject(body) || !Object.hasOwn(body, "prompt_cache_options")) return body;
|
|
350
|
+
const { prompt_cache_options: _options, ...rest } = body;
|
|
351
|
+
return rest;
|
|
352
|
+
}
|
|
353
|
+
|
|
181
354
|
/**
|
|
182
355
|
* A false model capability prevents Codex from emitting summary fields after the catalog refresh.
|
|
183
356
|
* Strip them here as well so an already-running client with a stale catalog cannot keep sending an
|
|
@@ -221,6 +394,27 @@ function stripDisabledReasoningSummaries(
|
|
|
221
394
|
};
|
|
222
395
|
}
|
|
223
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Hide a no-op Responses verbosity control from the wire as well as the catalog. This runs at
|
|
399
|
+
* final serialization so a stale catalog or direct caller cannot bypass the capability. Other
|
|
400
|
+
* `text` settings (notably structured-output `format`) remain untouched.
|
|
401
|
+
*/
|
|
402
|
+
function stripDisabledVerbosity(
|
|
403
|
+
body: unknown,
|
|
404
|
+
provider: OcxProviderConfig,
|
|
405
|
+
modelId: string,
|
|
406
|
+
): unknown {
|
|
407
|
+
if (modelRecordValue(provider.modelSupportsVerbosity, modelId) !== false || !isPlainObject(body)) {
|
|
408
|
+
return body;
|
|
409
|
+
}
|
|
410
|
+
if (!isPlainObject(body.text) || !Object.hasOwn(body.text, "verbosity")) return body;
|
|
411
|
+
const { verbosity: _verbosity, ...rest } = body.text;
|
|
412
|
+
return {
|
|
413
|
+
...body,
|
|
414
|
+
...(Object.keys(rest).length > 0 ? { text: rest } : { text: undefined }),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
224
418
|
/**
|
|
225
419
|
* Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
|
|
226
420
|
* did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
|
|
@@ -355,8 +549,12 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
|
355
549
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
356
550
|
}
|
|
357
551
|
|
|
358
|
-
function normalizeFunctionToolSchema(tool: unknown): unknown {
|
|
552
|
+
function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
|
|
359
553
|
if (!isPlainObject(tool) || tool.type !== "function") return tool;
|
|
554
|
+
if (xaiTarget) {
|
|
555
|
+
const parameters = normalizeXaiToolParameters(isPlainObject(tool.parameters) ? tool.parameters : {});
|
|
556
|
+
return parameters === undefined ? undefined : { ...tool, parameters };
|
|
557
|
+
}
|
|
360
558
|
if (isPlainObject(tool.parameters) && tool.parameters.type === "object") return tool;
|
|
361
559
|
return {
|
|
362
560
|
...tool,
|
|
@@ -364,16 +562,69 @@ function normalizeFunctionToolSchema(tool: unknown): unknown {
|
|
|
364
562
|
};
|
|
365
563
|
}
|
|
366
564
|
|
|
367
|
-
|
|
565
|
+
/**
|
|
566
|
+
* Re-point `tool_choice` after an incompatible function was dropped from the catalog. Names here
|
|
567
|
+
* are already wire names, because namespace lowering rewrote the declarations and the selector
|
|
568
|
+
* together before this runs. A selector left naming an omitted tool reaches Grok as a dangling
|
|
569
|
+
* reference it rejects, and silently relaxing it to `auto` is worse: the turn would quietly
|
|
570
|
+
* proceed without the tool the caller required. So an `allowed_tools` list drops the omitted
|
|
571
|
+
* entries while any remain, and a selection with nothing left to point at fails locally with the
|
|
572
|
+
* same 400 the caller gets for a tool catalog this proxy cannot lower.
|
|
573
|
+
*/
|
|
574
|
+
function reconcileToolChoiceForOmittedTools(
|
|
575
|
+
body: Record<string, unknown>,
|
|
576
|
+
omittedFunctionNames: ReadonlySet<string>,
|
|
577
|
+
): Record<string, unknown> {
|
|
578
|
+
if (omittedFunctionNames.size === 0) return body;
|
|
579
|
+
const toolChoice = body.tool_choice;
|
|
580
|
+
if (!isPlainObject(toolChoice)) return body;
|
|
581
|
+
|
|
582
|
+
const refuse = (name: string): never => {
|
|
583
|
+
throw new XaiToolSchemaCompatibilityError(
|
|
584
|
+
`tool_choice requires function "${name}", but its parameter schema cannot be represented for this destination; `
|
|
585
|
+
+ "relax tool_choice or simplify the tool's parameter schema",
|
|
586
|
+
);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
|
|
590
|
+
return omittedFunctionNames.has(toolChoice.name) ? refuse(toolChoice.name) : body;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
594
|
+
const omitted = toolChoice.tools.filter(tool =>
|
|
595
|
+
isPlainObject(tool)
|
|
596
|
+
&& tool.type === "function"
|
|
597
|
+
&& typeof tool.name === "string"
|
|
598
|
+
&& omittedFunctionNames.has(tool.name));
|
|
599
|
+
if (omitted.length === 0) return body;
|
|
600
|
+
const kept = toolChoice.tools.filter(tool => !omitted.includes(tool));
|
|
601
|
+
if (kept.length === 0) {
|
|
602
|
+
const first = omitted[0];
|
|
603
|
+
return refuse(isPlainObject(first) && typeof first.name === "string" ? first.name : "unknown");
|
|
604
|
+
}
|
|
605
|
+
return { ...body, tool_choice: { ...toolChoice, tools: kept } };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return body;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function normalizeToolSchemas(body: unknown, xaiTarget: boolean): unknown {
|
|
368
612
|
if (!isPlainObject(body)) return body;
|
|
369
613
|
|
|
614
|
+
const omittedFunctionNames = new Set<string>();
|
|
370
615
|
const normalizeTools = (tools: unknown[]): unknown[] => {
|
|
371
616
|
let changed = false;
|
|
372
|
-
const normalized =
|
|
373
|
-
|
|
617
|
+
const normalized: unknown[] = [];
|
|
618
|
+
for (const tool of tools) {
|
|
619
|
+
const fixed = normalizeFunctionToolSchema(tool, xaiTarget);
|
|
620
|
+
if (fixed === undefined) {
|
|
621
|
+
changed = true;
|
|
622
|
+
if (isPlainObject(tool) && typeof tool.name === "string") omittedFunctionNames.add(tool.name);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
374
625
|
if (fixed !== tool) changed = true;
|
|
375
|
-
|
|
376
|
-
}
|
|
626
|
+
normalized.push(fixed);
|
|
627
|
+
}
|
|
377
628
|
return changed ? normalized : tools;
|
|
378
629
|
};
|
|
379
630
|
|
|
@@ -393,7 +644,120 @@ function normalizeToolSchemas(body: unknown): unknown {
|
|
|
393
644
|
});
|
|
394
645
|
if (inputChanged) normalizedBody = { ...normalizedBody, input };
|
|
395
646
|
}
|
|
396
|
-
|
|
647
|
+
if (omittedFunctionNames.size > 0) {
|
|
648
|
+
// A dropped tool is a capability the caller declared and will not get, and the only other
|
|
649
|
+
// trace of it is a turn that never makes the call. Name them so the cause is recoverable.
|
|
650
|
+
debugProviderDiagnostic("openai-responses", "tool-schema-omitted", {
|
|
651
|
+
omitted: [...omittedFunctionNames],
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return reconcileToolChoiceForOmittedTools(normalizedBody, omittedFunctionNames);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function activateDeferredTool(tool: Record<string, unknown>): Record<string, unknown> {
|
|
658
|
+
const { defer_loading: _, ...activeTool } = tool;
|
|
659
|
+
if (tool.type !== "namespace" || !Array.isArray(tool.tools)) return activeTool;
|
|
660
|
+
return {
|
|
661
|
+
...activeTool,
|
|
662
|
+
tools: tool.tools.map(inner => isPlainObject(inner) ? activateDeferredTool(inner) : inner),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function mergeLoadedTools(declaredTools: unknown[], loadedTools: unknown[]): unknown[] {
|
|
667
|
+
const merged = [...declaredTools];
|
|
668
|
+
let changed = false;
|
|
669
|
+
|
|
670
|
+
for (const candidate of loadedTools) {
|
|
671
|
+
if (!isPlainObject(candidate) || typeof candidate.name !== "string") continue;
|
|
672
|
+
const loaded = activateDeferredTool(candidate);
|
|
673
|
+
if (loaded.type === "namespace" && Array.isArray(loaded.tools)) {
|
|
674
|
+
const namespaceIndex = merged.findIndex(tool =>
|
|
675
|
+
isPlainObject(tool) && tool.type === "namespace" && tool.name === loaded.name
|
|
676
|
+
);
|
|
677
|
+
if (namespaceIndex < 0) {
|
|
678
|
+
merged.push(loaded);
|
|
679
|
+
changed = true;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const namespace = merged[namespaceIndex];
|
|
684
|
+
if (!isPlainObject(namespace)) continue;
|
|
685
|
+
const namespaceTools = Array.isArray(namespace.tools) ? namespace.tools : [];
|
|
686
|
+
const nextNamespaceTools = [...namespaceTools];
|
|
687
|
+
let namespaceChanged = "defer_loading" in namespace;
|
|
688
|
+
for (const tool of loaded.tools) {
|
|
689
|
+
if (!isPlainObject(tool) || typeof tool.name !== "string") continue;
|
|
690
|
+
const declaredIndex = nextNamespaceTools.findIndex(declared =>
|
|
691
|
+
isPlainObject(declared) && declared.name === tool.name
|
|
692
|
+
);
|
|
693
|
+
if (declaredIndex < 0) {
|
|
694
|
+
nextNamespaceTools.push(tool);
|
|
695
|
+
namespaceChanged = true;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const declared = nextNamespaceTools[declaredIndex];
|
|
699
|
+
if (isPlainObject(declared) && "defer_loading" in declared) {
|
|
700
|
+
nextNamespaceTools[declaredIndex] = activateDeferredTool(declared);
|
|
701
|
+
namespaceChanged = true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!namespaceChanged) continue;
|
|
705
|
+
const { defer_loading: _, ...activeNamespace } = namespace;
|
|
706
|
+
merged[namespaceIndex] = { ...activeNamespace, tools: nextNamespaceTools };
|
|
707
|
+
changed = true;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const declaredIndex = merged.findIndex(tool =>
|
|
712
|
+
isPlainObject(tool) && tool.type !== "namespace" && tool.name === loaded.name
|
|
713
|
+
);
|
|
714
|
+
if (declaredIndex < 0) {
|
|
715
|
+
merged.push(loaded);
|
|
716
|
+
changed = true;
|
|
717
|
+
} else {
|
|
718
|
+
const declared = merged[declaredIndex];
|
|
719
|
+
if (isPlainObject(declared) && "defer_loading" in declared) {
|
|
720
|
+
merged[declaredIndex] = activateDeferredTool(declared);
|
|
721
|
+
changed = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return changed ? merged : declaredTools;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Client-executed tool search only changes Codex's parsed tool context. Routed passthrough keeps
|
|
731
|
+
* serializing the raw request, so activate those returned definitions for upstreams that do not
|
|
732
|
+
* implement the native deferred-loading handshake themselves.
|
|
733
|
+
*/
|
|
734
|
+
function promoteClientLoadedTools(body: unknown): unknown {
|
|
735
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
736
|
+
|
|
737
|
+
const loadedTools = body.input.flatMap(item =>
|
|
738
|
+
isPlainObject(item) && item.type === "tool_search_output" && Array.isArray(item.tools)
|
|
739
|
+
? item.tools
|
|
740
|
+
: []
|
|
741
|
+
);
|
|
742
|
+
if (loadedTools.length === 0) return body;
|
|
743
|
+
|
|
744
|
+
if (Array.isArray(body.tools)) {
|
|
745
|
+
const tools = mergeLoadedTools(body.tools, loadedTools);
|
|
746
|
+
return tools === body.tools ? body : { ...body, tools };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const additionalToolsIndex = body.input.findIndex(item =>
|
|
750
|
+
isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)
|
|
751
|
+
);
|
|
752
|
+
if (additionalToolsIndex < 0) return { ...body, tools: mergeLoadedTools([], loadedTools) };
|
|
753
|
+
|
|
754
|
+
const additionalTools = body.input[additionalToolsIndex];
|
|
755
|
+
if (!isPlainObject(additionalTools) || !Array.isArray(additionalTools.tools)) return body;
|
|
756
|
+
const tools = mergeLoadedTools(additionalTools.tools, loadedTools);
|
|
757
|
+
if (tools === additionalTools.tools) return body;
|
|
758
|
+
const input = [...body.input];
|
|
759
|
+
input[additionalToolsIndex] = { ...additionalTools, tools };
|
|
760
|
+
return { ...body, input };
|
|
397
761
|
}
|
|
398
762
|
|
|
399
763
|
const MAX_RESPONSES_CALL_ID_LENGTH = 64;
|
|
@@ -462,6 +826,15 @@ function toolOutputText(output: unknown): string {
|
|
|
462
826
|
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
|
|
463
827
|
* (the ChatGPT backend rejects it), so the delta may carry items that reference now-absent
|
|
464
828
|
* prior items and 400 upstream:
|
|
829
|
+
* - `function_call`/`local_shell_call`/`custom_tool_call` without their paired output item
|
|
830
|
+
* ("No tool output found for tool call <call_id>"). A stateless upstream cannot resolve
|
|
831
|
+
* the pair from its own storage, so a placeholder output is synthesized to keep the
|
|
832
|
+
* turn continuable without pretending the result was real. Synthetic outputs are
|
|
833
|
+
* emitted after the complete parallel call batch, in call order alongside any real
|
|
834
|
+
* outputs, so the adjacency normalizer can still recognize the batch as one
|
|
835
|
+
* reasoning-bearing assistant turn (#1477). Gated on
|
|
836
|
+
* `synthesizeMissingCallOutputs` (stateless AND non-forward wires); forward replay keeps
|
|
837
|
+
* fail-closed behavior.
|
|
465
838
|
* - `function_call_output`/`custom_tool_call_output` without their paired call item
|
|
466
839
|
* ("No tool call found for function call output with call_id ..."). Converted to user
|
|
467
840
|
* messages so the result text survives. `function_call_output` also pairs with
|
|
@@ -471,26 +844,65 @@ function toolOutputText(output: unknown): string {
|
|
|
471
844
|
* reasoning chain is intact and must be preserved.
|
|
472
845
|
* Runs on every forward request; with intact pairs it returns the original reference.
|
|
473
846
|
*/
|
|
474
|
-
|
|
847
|
+
/**
|
|
848
|
+
* Backfill `queries` on a replayed single-query `web_search_call`.
|
|
849
|
+
*
|
|
850
|
+
* `webSearchAction()` in the bridge now emits both keys, but that only helps items
|
|
851
|
+
* created after the fix. A conversation that already recorded
|
|
852
|
+
* `{type:"search", query:"..."}` replays that stored item on every subsequent turn, and
|
|
853
|
+
* DeepSeek's native Responses parser requires `queries` — so upgrading alone leaves
|
|
854
|
+
* those threads permanently 400ing with `missing field 'queries'` (#930).
|
|
855
|
+
*
|
|
856
|
+
* Runs on every Responses request, on both `input` items and the `action` nested inside
|
|
857
|
+
* them. Returns the original reference when nothing needs repair, so the common path
|
|
858
|
+
* allocates nothing.
|
|
859
|
+
*/
|
|
860
|
+
function backfillWebSearchQueries(body: unknown): unknown {
|
|
861
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
862
|
+
let changed = false;
|
|
863
|
+
const input = body.input.map(item => {
|
|
864
|
+
if (!isPlainObject(item) || item.type !== "web_search_call") return item;
|
|
865
|
+
const action = item.action;
|
|
866
|
+
if (!isPlainObject(action) || action.type !== "search") return item;
|
|
867
|
+
if (typeof action.query !== "string" || Array.isArray(action.queries)) return item;
|
|
868
|
+
changed = true;
|
|
869
|
+
return { ...item, action: { ...action, queries: [action.query] } };
|
|
870
|
+
});
|
|
871
|
+
return changed ? { ...body, input } : body;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthesizeMissingCallOutputs = false): unknown {
|
|
475
875
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
476
876
|
const input = body.input;
|
|
477
877
|
|
|
478
878
|
const functionCallIds = new Set<string>();
|
|
479
879
|
const customCallIds = new Set<string>();
|
|
880
|
+
const functionOutputIds = new Set<string>();
|
|
881
|
+
const customOutputIds = new Set<string>();
|
|
480
882
|
for (const item of input) {
|
|
481
883
|
if (!isPlainObject(item) || typeof item.call_id !== "string") continue;
|
|
482
884
|
if (item.type === "function_call" || item.type === "local_shell_call") functionCallIds.add(item.call_id);
|
|
483
885
|
else if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
|
|
886
|
+
else if (item.type === "function_call_output") functionOutputIds.add(item.call_id);
|
|
887
|
+
else if (item.type === "custom_tool_call_output") customOutputIds.add(item.call_id);
|
|
484
888
|
}
|
|
485
889
|
|
|
486
890
|
let changed = false;
|
|
487
891
|
const repaired: unknown[] = [];
|
|
892
|
+
const syntheticKeys = new Set<string>();
|
|
893
|
+
const pendingSyntheticOutputs: unknown[] = [];
|
|
894
|
+
const flushPendingSyntheticOutputs = (): void => {
|
|
895
|
+
if (pendingSyntheticOutputs.length === 0) return;
|
|
896
|
+
repaired.push(...pendingSyntheticOutputs);
|
|
897
|
+
pendingSyntheticOutputs.length = 0;
|
|
898
|
+
};
|
|
488
899
|
for (const item of input) {
|
|
489
|
-
if (!isPlainObject(item)) { repaired.push(item); continue; }
|
|
900
|
+
if (!isPlainObject(item)) { flushPendingSyntheticOutputs(); repaired.push(item); continue; }
|
|
490
901
|
if (dropReasoning && item.type === "reasoning") { changed = true; continue; }
|
|
491
902
|
const isFnOutput = item.type === "function_call_output";
|
|
492
903
|
const isCustomOutput = item.type === "custom_tool_call_output";
|
|
493
904
|
if (isFnOutput || isCustomOutput) {
|
|
905
|
+
flushPendingSyntheticOutputs();
|
|
494
906
|
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
495
907
|
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
|
|
496
908
|
if (!paired) {
|
|
@@ -503,10 +915,198 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknow
|
|
|
503
915
|
continue;
|
|
504
916
|
}
|
|
505
917
|
}
|
|
918
|
+
const isFnCall = item.type === "function_call" || item.type === "local_shell_call";
|
|
919
|
+
const isCustomCall = item.type === "custom_tool_call";
|
|
920
|
+
if (isFnCall || isCustomCall) {
|
|
921
|
+
repaired.push(item);
|
|
922
|
+
if (synthesizeMissingCallOutputs) {
|
|
923
|
+
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
924
|
+
const hasOutput = isFnCall ? functionOutputIds.has(callId) : customOutputIds.has(callId);
|
|
925
|
+
if (!hasOutput && callId) {
|
|
926
|
+
changed = true;
|
|
927
|
+
const name = typeof item.name === "string" && item.name.length > 0 ? item.name : callId;
|
|
928
|
+
const text = `[ocx] no tool result was recorded for "${name}"; execution status unknown — do not treat this as success, failure, or user-provided input.`;
|
|
929
|
+
syntheticKeys.add(`${isFnCall ? "function" : "custom"}:${callId}`);
|
|
930
|
+
pendingSyntheticOutputs.push(isFnCall
|
|
931
|
+
? { type: "function_call_output", call_id: callId, output: text }
|
|
932
|
+
: { type: "custom_tool_call_output", call_id: callId, output: text });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
flushPendingSyntheticOutputs();
|
|
506
938
|
repaired.push(item);
|
|
507
939
|
}
|
|
940
|
+
flushPendingSyntheticOutputs();
|
|
941
|
+
|
|
942
|
+
const callKeyOf = (item: unknown): string | null => {
|
|
943
|
+
if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
|
|
944
|
+
if (item.type === "function_call" || item.type === "local_shell_call") return `function:${item.call_id}`;
|
|
945
|
+
if (item.type === "custom_tool_call") return `custom:${item.call_id}`;
|
|
946
|
+
return null;
|
|
947
|
+
};
|
|
948
|
+
const outputKeyOf = (item: unknown): string | null => {
|
|
949
|
+
if (!isPlainObject(item) || typeof item.call_id !== "string") return null;
|
|
950
|
+
if (item.type === "function_call_output") return `function:${item.call_id}`;
|
|
951
|
+
if (item.type === "custom_tool_call_output") return `custom:${item.call_id}`;
|
|
952
|
+
return null;
|
|
953
|
+
};
|
|
954
|
+
const reorderBatchOutputs = (items: unknown[]): unknown[] => {
|
|
955
|
+
const ordered: unknown[] = [];
|
|
956
|
+
const claimedOutputIndexes = new Set<number>();
|
|
957
|
+
const outputIndexesByKey = new Map<string, { indexes: number[]; offset: number }>();
|
|
958
|
+
for (let outputIndex = 0; outputIndex < items.length; outputIndex += 1) {
|
|
959
|
+
const outputKey = outputKeyOf(items[outputIndex]);
|
|
960
|
+
if (outputKey === null) continue;
|
|
961
|
+
const bucket = outputIndexesByKey.get(outputKey);
|
|
962
|
+
if (bucket) bucket.indexes.push(outputIndex);
|
|
963
|
+
else outputIndexesByKey.set(outputKey, { indexes: [outputIndex], offset: 0 });
|
|
964
|
+
}
|
|
965
|
+
let index = 0;
|
|
966
|
+
while (index < items.length) {
|
|
967
|
+
if (claimedOutputIndexes.has(index)) { index += 1; continue; }
|
|
968
|
+
const key = callKeyOf(items[index]);
|
|
969
|
+
if (key === null) { ordered.push(items[index]); index += 1; continue; }
|
|
970
|
+
const batch: unknown[] = [];
|
|
971
|
+
const batchKeys: string[] = [];
|
|
972
|
+
let cursor = index;
|
|
973
|
+
while (cursor < items.length) {
|
|
974
|
+
const nextKey = callKeyOf(items[cursor]);
|
|
975
|
+
if (nextKey === null) break;
|
|
976
|
+
batch.push(items[cursor]);
|
|
977
|
+
batchKeys.push(nextKey);
|
|
978
|
+
cursor += 1;
|
|
979
|
+
}
|
|
980
|
+
const hasSynthetic = batchKeys.some(batchKey => syntheticKeys.has(batchKey));
|
|
981
|
+
if (!hasSynthetic) {
|
|
982
|
+
ordered.push(...batch);
|
|
983
|
+
index = cursor;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const batchOutputs: unknown[] = [];
|
|
987
|
+
for (const batchKey of batchKeys) {
|
|
988
|
+
const bucket = outputIndexesByKey.get(batchKey);
|
|
989
|
+
if (!bucket) continue;
|
|
990
|
+
while (bucket.offset < bucket.indexes.length && bucket.indexes[bucket.offset]! < cursor) {
|
|
991
|
+
bucket.offset += 1;
|
|
992
|
+
}
|
|
993
|
+
while (bucket.offset < bucket.indexes.length) {
|
|
994
|
+
const outputIndex = bucket.indexes[bucket.offset]!;
|
|
995
|
+
bucket.offset += 1;
|
|
996
|
+
if (claimedOutputIndexes.has(outputIndex)) continue;
|
|
997
|
+
claimedOutputIndexes.add(outputIndex);
|
|
998
|
+
batchOutputs.push(items[outputIndex]);
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
ordered.push(...batch, ...batchOutputs);
|
|
1003
|
+
index = cursor;
|
|
1004
|
+
}
|
|
1005
|
+
return ordered;
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
return changed ? { ...body, input: reorderBatchOutputs(repaired) } : body;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Make unambiguous Responses tool batches contiguous for upstream parsers that require it.
|
|
1013
|
+
*
|
|
1014
|
+
* [Decision Log]
|
|
1015
|
+
* - 목적과 의도: Keep Codex hook-injected developer context without splitting a parallel tool-call turn away from its reasoning or making a strict upstream reject matching results.
|
|
1016
|
+
* - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence, while the original pair-by-pair reorder turned `reasoning, call A, call B, output A, output B` into two assistant turns and made DeepSeek reject call B for missing reasoning (#1477).
|
|
1017
|
+
* - 검토한 주요 대안: Disable parallel calls (DeepSeek always enables them); duplicate reasoning per call; reorder each pair; or normalize the complete unambiguous call batch.
|
|
1018
|
+
* - 선택한 방식: Treat calls emitted before the first matched result as one batch, emit all calls followed by their matched outputs, and preserve intervening non-tool items immediately after the batch.
|
|
1019
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: Batch normalization matches the Responses parallel-call shape without fabricating reasoning, while the provider gate and unique-pair requirement keep the blast radius narrow.
|
|
1020
|
+
* - 장점, 단점 및 영향: DeepSeek keeps one reasoning-bearing assistant turn for parallel calls and still accepts hook-interleaved single calls; tolerant providers stay byte/order equivalent, and duplicate, missing, or backwards call/result pairs are not guessed.
|
|
1021
|
+
*/
|
|
1022
|
+
function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
|
|
1023
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
1024
|
+
const input = body.input;
|
|
1025
|
+
const calls = new Map<string, number[]>();
|
|
1026
|
+
const outputs = new Map<string, number[]>();
|
|
1027
|
+
|
|
1028
|
+
const appendIndex = (map: Map<string, number[]>, key: string, index: number): void => {
|
|
1029
|
+
const existing = map.get(key);
|
|
1030
|
+
if (existing) existing.push(index);
|
|
1031
|
+
else map.set(key, [index]);
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1035
|
+
const item = input[index];
|
|
1036
|
+
if (!isPlainObject(item) || typeof item.call_id !== "string" || item.call_id.length === 0) continue;
|
|
1037
|
+
if (item.type === "function_call" || item.type === "local_shell_call") {
|
|
1038
|
+
appendIndex(calls, `function:${item.call_id}`, index);
|
|
1039
|
+
} else if (item.type === "custom_tool_call") {
|
|
1040
|
+
appendIndex(calls, `custom:${item.call_id}`, index);
|
|
1041
|
+
} else if (item.type === "function_call_output") {
|
|
1042
|
+
appendIndex(outputs, `function:${item.call_id}`, index);
|
|
1043
|
+
} else if (item.type === "custom_tool_call_output") {
|
|
1044
|
+
appendIndex(outputs, `custom:${item.call_id}`, index);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
|
|
1049
|
+
for (const [key, callIndices] of calls) {
|
|
1050
|
+
const outputIndices = outputs.get(key);
|
|
1051
|
+
if (!outputIndices) return body;
|
|
1052
|
+
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
|
|
1053
|
+
const callIndex = callIndices[0]!;
|
|
1054
|
+
const outputIndex = outputIndices[0]!;
|
|
1055
|
+
if (outputIndex <= callIndex) return body;
|
|
1056
|
+
pairs.push({ callIndex, outputIndex });
|
|
1057
|
+
}
|
|
1058
|
+
// Reject any collected output that lacks exactly one matching call. A lone or
|
|
1059
|
+
// duplicated output is ambiguous, and normalizing on top of it could sever a
|
|
1060
|
+
// result from the reasoning-bearing call turn it belongs to.
|
|
1061
|
+
for (const [key, outputIndices] of outputs) {
|
|
1062
|
+
const callIndices = calls.get(key);
|
|
1063
|
+
if (!callIndices || callIndices.length !== 1 || outputIndices.length !== 1) return body;
|
|
1064
|
+
}
|
|
1065
|
+
pairs.sort((left, right) => left.callIndex - right.callIndex);
|
|
1066
|
+
|
|
1067
|
+
const movedIndices = new Set<number>();
|
|
1068
|
+
const batchAt = new Map<number, unknown[]>();
|
|
1069
|
+
for (let cursor = 0; cursor < pairs.length;) {
|
|
1070
|
+
const group = [pairs[cursor]!];
|
|
1071
|
+
let firstOutputIndex = pairs[cursor]!.outputIndex;
|
|
1072
|
+
let next = cursor + 1;
|
|
1073
|
+
while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) {
|
|
1074
|
+
group.push(pairs[next]!);
|
|
1075
|
+
firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex);
|
|
1076
|
+
next += 1;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Within one reasoning turn the outputs must appear in the same order as their
|
|
1080
|
+
// calls. If they are reversed, normalizing would fabricate a new output order;
|
|
1081
|
+
// leave the ambiguous history untouched instead.
|
|
1082
|
+
for (let groupIndex = 1; groupIndex < group.length; groupIndex += 1) {
|
|
1083
|
+
if (group[groupIndex]!.outputIndex < group[groupIndex - 1]!.outputIndex) return body;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const batch = [
|
|
1087
|
+
...group.map(pair => input[pair.callIndex]),
|
|
1088
|
+
...group.map(pair => input[pair.outputIndex]),
|
|
1089
|
+
];
|
|
1090
|
+
const anchor = group[0]!.callIndex;
|
|
1091
|
+
const alreadyContiguous = batch.every((item, offset) => input[anchor + offset] === item);
|
|
1092
|
+
if (!alreadyContiguous) {
|
|
1093
|
+
batchAt.set(anchor, batch);
|
|
1094
|
+
for (const pair of group) {
|
|
1095
|
+
movedIndices.add(pair.callIndex);
|
|
1096
|
+
movedIndices.add(pair.outputIndex);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
cursor = next;
|
|
1100
|
+
}
|
|
1101
|
+
if (batchAt.size === 0) return body;
|
|
508
1102
|
|
|
509
|
-
|
|
1103
|
+
const normalized: unknown[] = [];
|
|
1104
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1105
|
+
const batch = batchAt.get(index);
|
|
1106
|
+
if (batch) normalized.push(...batch);
|
|
1107
|
+
if (!movedIndices.has(index)) normalized.push(input[index]);
|
|
1108
|
+
}
|
|
1109
|
+
return { ...body, input: normalized };
|
|
510
1110
|
}
|
|
511
1111
|
|
|
512
1112
|
/**
|
|
@@ -526,6 +1126,15 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
|
|
|
526
1126
|
return rest;
|
|
527
1127
|
}
|
|
528
1128
|
|
|
1129
|
+
/** Apply the settled tier only to a fresh outbound object; `_rawBody` remains caller-owned. */
|
|
1130
|
+
function applyTierDecisionToResponsesBody(body: unknown, decision: TierDecision | undefined): unknown {
|
|
1131
|
+
if (!decision || decision.kind === "forward-caller" || !isPlainObject(body)) return body;
|
|
1132
|
+
const next: Record<string, unknown> = { ...body };
|
|
1133
|
+
if (decision.kind === "set") next.service_tier = decision.value;
|
|
1134
|
+
else delete next.service_tier;
|
|
1135
|
+
return next;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
529
1138
|
/**
|
|
530
1139
|
* Drop request parameters a stateless Responses upstream cannot implement, and pin
|
|
531
1140
|
* `store` false.
|
|
@@ -540,8 +1149,8 @@ function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
|
|
|
540
1149
|
* `prompt` is a reference to a server-stored prompt template — the most stateful
|
|
541
1150
|
* field in the accepted schema.
|
|
542
1151
|
*
|
|
543
|
-
* `service_tier` is deliberately NOT dropped: the
|
|
544
|
-
*
|
|
1152
|
+
* `service_tier` is deliberately NOT dropped: the final TierDecision is applied to a
|
|
1153
|
+
* detached outbound body before this sanitizer chain, and silently deleting a configured knob is
|
|
545
1154
|
* worse than forwarding a parameter the upstream ignores.
|
|
546
1155
|
*
|
|
547
1156
|
* MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
|
|
@@ -577,6 +1186,143 @@ function stripUnsupportedForwardParams(body: unknown): unknown {
|
|
|
577
1186
|
return rest;
|
|
578
1187
|
}
|
|
579
1188
|
|
|
1189
|
+
/** Return the lossless text represented by one system message, or null when it is multimodal. */
|
|
1190
|
+
function canonicalForwardSystemText(item: Record<string, unknown>): string | null {
|
|
1191
|
+
const content = item.content;
|
|
1192
|
+
if (content === undefined) return "";
|
|
1193
|
+
if (typeof content === "string") return content;
|
|
1194
|
+
if (!Array.isArray(content)) return null;
|
|
1195
|
+
let text = "";
|
|
1196
|
+
for (const block of content) {
|
|
1197
|
+
if (!isPlainObject(block)) return null;
|
|
1198
|
+
if (block.type !== "input_text" && block.type !== "text") return null;
|
|
1199
|
+
if (typeof block.text !== "string") return null;
|
|
1200
|
+
text += block.text;
|
|
1201
|
+
}
|
|
1202
|
+
return text;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* The public Responses API accepts input system messages and `truncation`, but the canonical
|
|
1207
|
+
* ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the
|
|
1208
|
+
* existing top-level instructions and remove the unsupported flag at this destination boundary.
|
|
1209
|
+
*
|
|
1210
|
+
* The fold is atomic: if any system message contains a non-text block, keep every message in
|
|
1211
|
+
* place so the proxy never silently drops multimodal content. The backend may still reject that
|
|
1212
|
+
* unsupported shape, but it will not receive a partially rewritten prompt.
|
|
1213
|
+
*/
|
|
1214
|
+
function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
|
|
1215
|
+
if (!isPlainObject(body)) return body;
|
|
1216
|
+
const stripTruncation = Object.hasOwn(body, "truncation");
|
|
1217
|
+
const input = Array.isArray(body.input) ? body.input : undefined;
|
|
1218
|
+
if (!input) {
|
|
1219
|
+
if (!stripTruncation) return body;
|
|
1220
|
+
const { truncation: _truncation, ...rest } = body;
|
|
1221
|
+
return rest;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const foldedText: string[] = [];
|
|
1225
|
+
let sawSystemMessage = false;
|
|
1226
|
+
let canFoldAllSystemMessages = true;
|
|
1227
|
+
for (const item of input) {
|
|
1228
|
+
if (!isPlainObject(item) || item.role !== "system") continue;
|
|
1229
|
+
sawSystemMessage = true;
|
|
1230
|
+
const text = canonicalForwardSystemText(item);
|
|
1231
|
+
if (text === null) {
|
|
1232
|
+
canFoldAllSystemMessages = false;
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
foldedText.push(text);
|
|
1236
|
+
}
|
|
1237
|
+
if (!stripTruncation && (!sawSystemMessage || !canFoldAllSystemMessages)) return body;
|
|
1238
|
+
|
|
1239
|
+
const next: Record<string, unknown> = { ...body };
|
|
1240
|
+
if (stripTruncation) delete next.truncation;
|
|
1241
|
+
if (sawSystemMessage && canFoldAllSystemMessages) {
|
|
1242
|
+
next.input = input.filter(item => !isPlainObject(item) || item.role !== "system");
|
|
1243
|
+
const folded = foldedText.join("\n\n");
|
|
1244
|
+
if (folded !== "") {
|
|
1245
|
+
const existing = typeof body.instructions === "string" ? body.instructions : "";
|
|
1246
|
+
next.instructions = existing !== "" ? `${existing}\n\n${folded}` : folded;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return next;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const POSIT_CACHE_MARKER_MAX_DEPTH = 64;
|
|
1253
|
+
const POSIT_CACHE_MARKER_MAX_NODES = 100_000;
|
|
1254
|
+
|
|
1255
|
+
type PromptCacheMarkerRewrite = {
|
|
1256
|
+
value: unknown;
|
|
1257
|
+
changed: boolean;
|
|
1258
|
+
complete: boolean;
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Remove Posit/Anthropic-style prompt-cache markers without trusting request nesting. The walk
|
|
1263
|
+
* aborts atomically when its depth or node budget is exceeded, so a hostile extension object can
|
|
1264
|
+
* neither overflow the stack nor receive a partially rewritten subtree.
|
|
1265
|
+
*/
|
|
1266
|
+
function stripPromptCacheBreakpoints(
|
|
1267
|
+
value: unknown,
|
|
1268
|
+
state: { nodes: number },
|
|
1269
|
+
depth = 0,
|
|
1270
|
+
): PromptCacheMarkerRewrite {
|
|
1271
|
+
state.nodes += 1;
|
|
1272
|
+
if (depth > POSIT_CACHE_MARKER_MAX_DEPTH || state.nodes > POSIT_CACHE_MARKER_MAX_NODES) {
|
|
1273
|
+
return { value, changed: false, complete: false };
|
|
1274
|
+
}
|
|
1275
|
+
if (Array.isArray(value)) {
|
|
1276
|
+
let changed = false;
|
|
1277
|
+
const next: unknown[] = [];
|
|
1278
|
+
for (const entry of value) {
|
|
1279
|
+
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
|
|
1280
|
+
if (!rewritten.complete) return { value, changed: false, complete: false };
|
|
1281
|
+
changed ||= rewritten.changed;
|
|
1282
|
+
next.push(rewritten.value);
|
|
1283
|
+
}
|
|
1284
|
+
return { value: changed ? next : value, changed, complete: true };
|
|
1285
|
+
}
|
|
1286
|
+
if (!isPlainObject(value)) return { value, changed: false, complete: true };
|
|
1287
|
+
|
|
1288
|
+
let changed = Object.hasOwn(value, "prompt_cache_breakpoint");
|
|
1289
|
+
const next: Record<string, unknown> = {};
|
|
1290
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1291
|
+
if (key === "prompt_cache_breakpoint") continue;
|
|
1292
|
+
const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
|
|
1293
|
+
if (!rewritten.complete) return { value, changed: false, complete: false };
|
|
1294
|
+
changed ||= rewritten.changed;
|
|
1295
|
+
next[key] = rewritten.value;
|
|
1296
|
+
}
|
|
1297
|
+
return { value: changed ? next : value, changed, complete: true };
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Posit Assistant can replay client-only cache markers and stored-item references on a
|
|
1302
|
+
* `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the
|
|
1303
|
+
* markers recursively and drop only `item_reference` rows that cannot name persisted state;
|
|
1304
|
+
* ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain.
|
|
1305
|
+
*/
|
|
1306
|
+
function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown {
|
|
1307
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
1308
|
+
let input: unknown[] = body.input;
|
|
1309
|
+
let changed = false;
|
|
1310
|
+
if (body.store === false) {
|
|
1311
|
+
const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference");
|
|
1312
|
+
if (withoutReferences.length !== input.length) {
|
|
1313
|
+
input = withoutReferences;
|
|
1314
|
+
changed = true;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 });
|
|
1319
|
+
if (markerRewrite.complete && markerRewrite.changed) {
|
|
1320
|
+
input = markerRewrite.value as unknown[];
|
|
1321
|
+
changed = true;
|
|
1322
|
+
}
|
|
1323
|
+
return changed ? { ...body, input } : body;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
580
1326
|
const IMAGE_GEN_NAMESPACE = "image_gen";
|
|
581
1327
|
const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
|
|
582
1328
|
const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
|
|
@@ -608,6 +1354,128 @@ function declaresImageGenClientTool(tool: unknown): boolean {
|
|
|
608
1354
|
return isImageGenClientName(tool.name);
|
|
609
1355
|
}
|
|
610
1356
|
|
|
1357
|
+
/** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
|
|
1358
|
+
function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
|
|
1359
|
+
if (!isPlainObject(toolChoice)) return toolChoice;
|
|
1360
|
+
if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
|
|
1361
|
+
return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
|
|
1362
|
+
}
|
|
1363
|
+
if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
|
|
1364
|
+
const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
|
|
1365
|
+
let changed = false;
|
|
1366
|
+
let addedHostedImageTool = false;
|
|
1367
|
+
const tools: unknown[] = [];
|
|
1368
|
+
for (const tool of toolChoice.tools) {
|
|
1369
|
+
const isClientImageTool = isPlainObject(tool)
|
|
1370
|
+
&& (tool.type === "function" || tool.type === "custom")
|
|
1371
|
+
&& typeof tool.name === "string"
|
|
1372
|
+
&& isImageGenClientName(tool.name);
|
|
1373
|
+
if (!isClientImageTool) {
|
|
1374
|
+
tools.push(tool);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
changed = true;
|
|
1378
|
+
if (!hasHostedImageTool && !addedHostedImageTool) {
|
|
1379
|
+
tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
|
|
1380
|
+
addedHostedImageTool = true;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return changed ? { ...toolChoice, tools } : toolChoice;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Some Responses-compatible gateways reserve the hosted image namespace even when the request
|
|
1388
|
+
* does not explicitly declare `image_generation`. For an explicitly configured model, remove only
|
|
1389
|
+
* colliding client declarations so the gateway's hosted tool can take precedence.
|
|
1390
|
+
*/
|
|
1391
|
+
function preferConfiguredHostedTools(
|
|
1392
|
+
body: unknown,
|
|
1393
|
+
provider: OcxProviderConfig,
|
|
1394
|
+
modelId: string,
|
|
1395
|
+
selectedModelId?: string,
|
|
1396
|
+
): unknown {
|
|
1397
|
+
// A virtual model's advertised id takes precedence over its resolved wire-model id.
|
|
1398
|
+
// Read own properties only: a routed model id of `constructor`/`toString` would
|
|
1399
|
+
// otherwise resolve to an inherited Object.prototype function and throw on the
|
|
1400
|
+
// membership test below, failing the request before it is dispatched.
|
|
1401
|
+
const preferenceMap = provider.modelPreferHostedTools;
|
|
1402
|
+
const ownPreference = (key: string | undefined): string[] | undefined => {
|
|
1403
|
+
if (!key || !preferenceMap || !Object.prototype.hasOwnProperty.call(preferenceMap, key)) return undefined;
|
|
1404
|
+
const entry = preferenceMap[key];
|
|
1405
|
+
return Array.isArray(entry) ? entry : undefined;
|
|
1406
|
+
};
|
|
1407
|
+
const preferredTools = ownPreference(selectedModelId) ?? ownPreference(modelId);
|
|
1408
|
+
if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;
|
|
1409
|
+
|
|
1410
|
+
const stripGroup = (tools: unknown[]): unknown[] => {
|
|
1411
|
+
const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
|
|
1412
|
+
return filtered.length === tools.length ? tools : filtered;
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
let changed = false;
|
|
1416
|
+
let tools = body.tools;
|
|
1417
|
+
let strippedTopLevelImageGenTool = false;
|
|
1418
|
+
if (Array.isArray(body.tools)) {
|
|
1419
|
+
tools = stripGroup(body.tools);
|
|
1420
|
+
strippedTopLevelImageGenTool = tools !== body.tools;
|
|
1421
|
+
changed ||= strippedTopLevelImageGenTool;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
let input = body.input;
|
|
1425
|
+
const strippedAdditionalToolsIndices = new Set<number>();
|
|
1426
|
+
if (Array.isArray(body.input)) {
|
|
1427
|
+
let nestedChanged = false;
|
|
1428
|
+
const mappedInput = body.input.map((item, index) => {
|
|
1429
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
1430
|
+
const nestedTools = stripGroup(item.tools);
|
|
1431
|
+
if (nestedTools === item.tools) return item;
|
|
1432
|
+
strippedAdditionalToolsIndices.add(index);
|
|
1433
|
+
nestedChanged = true;
|
|
1434
|
+
return { ...item, tools: nestedTools };
|
|
1435
|
+
});
|
|
1436
|
+
if (nestedChanged) {
|
|
1437
|
+
input = mappedInput;
|
|
1438
|
+
changed = true;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const hasToolChoice = Object.hasOwn(body, "tool_choice");
|
|
1443
|
+
const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
|
|
1444
|
+
const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
|
|
1445
|
+
const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
|
|
1446
|
+
&& toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
|
|
1447
|
+
const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
|
|
1448
|
+
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
|
|
1449
|
+
&& item.type === "additional_tools"
|
|
1450
|
+
&& hasHostedImageGenTool(item.tools)));
|
|
1451
|
+
if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
|
|
1452
|
+
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
|
|
1453
|
+
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
|
|
1454
|
+
} else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
|
|
1455
|
+
// Restore into the FIRST stripped container only. Tool declarations are
|
|
1456
|
+
// request-scoped, not container-scoped — the containers are separate carriers for
|
|
1457
|
+
// one tool set, so a single hosted declaration covers the request. An earlier
|
|
1458
|
+
// revision restored into every stripped container and put `image_generation` on
|
|
1459
|
+
// the wire twice; review caught it.
|
|
1460
|
+
const firstStripped = Math.min(...strippedAdditionalToolsIndices);
|
|
1461
|
+
input = input.map((item, index) => index === firstStripped
|
|
1462
|
+
&& isPlainObject(item)
|
|
1463
|
+
&& Array.isArray(item.tools)
|
|
1464
|
+
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
|
|
1465
|
+
: item);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
changed ||= toolChoiceChanged;
|
|
1469
|
+
if (!changed) return body;
|
|
1470
|
+
const next: Record<string, unknown> = {
|
|
1471
|
+
...body,
|
|
1472
|
+
...(Array.isArray(body.tools) ? { tools } : {}),
|
|
1473
|
+
...(Array.isArray(body.input) ? { input } : {}),
|
|
1474
|
+
};
|
|
1475
|
+
if (toolChoiceChanged) next.tool_choice = toolChoice;
|
|
1476
|
+
return next;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
611
1479
|
/**
|
|
612
1480
|
* Lower one complete Codex image-gen namespace to public Responses function tools.
|
|
613
1481
|
*
|
|
@@ -866,17 +1734,126 @@ function normalizeImageGenClientTools(body: unknown): unknown {
|
|
|
866
1734
|
function stripUnsupportedHostedTools(body: unknown): unknown {
|
|
867
1735
|
if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
|
|
868
1736
|
const model = typeof body.model === "string" ? body.model : "";
|
|
869
|
-
const unsupported = UNSUPPORTED_HOSTED_TOOLS.filter(e => e.match(model));
|
|
870
|
-
if (unsupported.length === 0) return body;
|
|
871
|
-
|
|
872
1737
|
const tools = body.tools.filter(t => {
|
|
873
1738
|
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
874
|
-
|
|
875
|
-
return !unsupported.some(e => e.tools.has(type));
|
|
1739
|
+
return !type || !isHostedToolUnsupportedForModel(model, type);
|
|
876
1740
|
});
|
|
877
1741
|
return tools.length === body.tools.length ? body : { ...body, tools };
|
|
878
1742
|
}
|
|
879
1743
|
|
|
1744
|
+
/**
|
|
1745
|
+
* OpenAI hosted web_search config fields that a capability-classified Responses
|
|
1746
|
+
* upstream may reject wholesale. xAI's /v1/responses 400s the entire request on
|
|
1747
|
+
* `external_web_access` and `search_context_size` ("Argument not supported"),
|
|
1748
|
+
* which killed every routed Grok turn whose client (Codex) attaches its
|
|
1749
|
+
* default web_search tool config (probe 2026-08-21: both fields 400
|
|
1750
|
+
* individually; `user_location` and `filters` are accepted and kept).
|
|
1751
|
+
* The caller decides whether to apply this compatibility transform from explicit
|
|
1752
|
+
* provider capability metadata; an unclassified upstream keeps the fields.
|
|
1753
|
+
*/
|
|
1754
|
+
const OPENAI_ONLY_WEB_SEARCH_FIELDS = ["external_web_access", "search_context_size"] as const;
|
|
1755
|
+
|
|
1756
|
+
function stripOpenAiOnlyWebSearchFieldsFromTools(tools: unknown[]): {
|
|
1757
|
+
tools: unknown[];
|
|
1758
|
+
changed: boolean;
|
|
1759
|
+
} {
|
|
1760
|
+
let changed = false;
|
|
1761
|
+
const stripped = tools.map(tool => {
|
|
1762
|
+
if (!isPlainObject(tool) || (tool.type !== "web_search" && tool.type !== "web_search_preview")) {
|
|
1763
|
+
return tool;
|
|
1764
|
+
}
|
|
1765
|
+
if (!OPENAI_ONLY_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) return tool;
|
|
1766
|
+
const { external_web_access: _access, search_context_size: _size, ...rest } = tool;
|
|
1767
|
+
changed = true;
|
|
1768
|
+
return rest;
|
|
1769
|
+
});
|
|
1770
|
+
return { tools: changed ? stripped : tools, changed };
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
|
|
1774
|
+
if (!isPlainObject(body)) return body;
|
|
1775
|
+
|
|
1776
|
+
let next: Record<string, unknown> = body;
|
|
1777
|
+
let changed = false;
|
|
1778
|
+
if (Array.isArray(body.tools)) {
|
|
1779
|
+
const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(body.tools);
|
|
1780
|
+
if (stripped.changed) {
|
|
1781
|
+
next = { ...next, tools: stripped.tools };
|
|
1782
|
+
changed = true;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (Array.isArray(body.input)) {
|
|
1787
|
+
let inputChanged = false;
|
|
1788
|
+
const input = body.input.map(item => {
|
|
1789
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
|
|
1790
|
+
return item;
|
|
1791
|
+
}
|
|
1792
|
+
const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(item.tools);
|
|
1793
|
+
if (!stripped.changed) return item;
|
|
1794
|
+
inputChanged = true;
|
|
1795
|
+
return { ...item, tools: stripped.tools };
|
|
1796
|
+
});
|
|
1797
|
+
if (inputChanged) {
|
|
1798
|
+
next = { ...next, input };
|
|
1799
|
+
changed = true;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
return changed ? next : body;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* OpenCode Zen / Go Muse Spark Responses gateway refuses `search_content_types`
|
|
1808
|
+
* on a plain `web_search` tool (400) but accepts it on `web_search_preview`; a
|
|
1809
|
+
* plain `web_search` is also accepted. Probed directly against the gateway on
|
|
1810
|
+
* 2026-08-26: `web_search` + `search_content_types` -> 400, `web_search_preview`
|
|
1811
|
+
* + `search_content_types` -> 200, plain `web_search` -> 200. Luna accepts every
|
|
1812
|
+
* shape, so this is Muse-only. Drop only the field the gateway refuses while
|
|
1813
|
+
* keeping the tool type and every other accepted option intact.
|
|
1814
|
+
*/
|
|
1815
|
+
function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown {
|
|
1816
|
+
if (!isPlainObject(body)) return body;
|
|
1817
|
+
if (typeof modelId !== "string" || modelId.trim().toLowerCase() !== "muse-spark-1.2-contributor") return body;
|
|
1818
|
+
|
|
1819
|
+
const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
|
|
1820
|
+
let changed = false;
|
|
1821
|
+
const rewritten = tools.map(tool => {
|
|
1822
|
+
if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
|
|
1823
|
+
if (!Object.hasOwn(tool, "search_content_types")) return tool;
|
|
1824
|
+
const { search_content_types: _dropped, ...rest } = tool;
|
|
1825
|
+
changed = true;
|
|
1826
|
+
return rest;
|
|
1827
|
+
});
|
|
1828
|
+
return { tools: changed ? rewritten : tools, changed };
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
let next: Record<string, unknown> = body;
|
|
1832
|
+
let changed = false;
|
|
1833
|
+
if (Array.isArray(body.tools)) {
|
|
1834
|
+
const rewritten = rewriteTools(body.tools);
|
|
1835
|
+
if (rewritten.changed) {
|
|
1836
|
+
next = { ...next, tools: rewritten.tools };
|
|
1837
|
+
changed = true;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (Array.isArray(next.input)) {
|
|
1841
|
+
let inputChanged = false;
|
|
1842
|
+
const input = next.input.map(item => {
|
|
1843
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
1844
|
+
const rewritten = rewriteTools(item.tools);
|
|
1845
|
+
if (!rewritten.changed) return item;
|
|
1846
|
+
inputChanged = true;
|
|
1847
|
+
return { ...item, tools: rewritten.tools };
|
|
1848
|
+
});
|
|
1849
|
+
if (inputChanged) {
|
|
1850
|
+
next = { ...next, input };
|
|
1851
|
+
changed = true;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return changed ? next : body;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
880
1857
|
/** Replace every `input_image` part under a routed-compaction body with a short marker. */
|
|
881
1858
|
function stripInputImagesDeep(value: unknown): unknown {
|
|
882
1859
|
if (Array.isArray(value)) return value.map(stripInputImagesDeep);
|
|
@@ -901,7 +1878,8 @@ function stripInputImagesDeep(value: unknown): unknown {
|
|
|
901
1878
|
*/
|
|
902
1879
|
function buildRoutedCompactionBody(body: unknown): unknown {
|
|
903
1880
|
if (!isPlainObject(body)) return body;
|
|
904
|
-
|
|
1881
|
+
// `text` goes with the tool fields: the summary must be prose, not schema-constrained JSON.
|
|
1882
|
+
const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, text: _text, ...rest } = body;
|
|
905
1883
|
const input = Array.isArray(body.input) ? body.input : [];
|
|
906
1884
|
const kept = input.filter(item => !isPlainObject(item)
|
|
907
1885
|
// `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
|
|
@@ -963,29 +1941,38 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
963
1941
|
let url: string;
|
|
964
1942
|
|
|
965
1943
|
if (provider.authMode === "forward") {
|
|
1944
|
+
const mayForwardCallerCredentials = isCanonicalOpenAiForwardProvider(provider);
|
|
966
1945
|
// OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
|
|
967
|
-
|
|
1946
|
+
const baseUrl = mayForwardCallerCredentials
|
|
1947
|
+
? CODEX_FORWARD_BASE_URL
|
|
1948
|
+
: provider.baseUrl.replace(/\/+$/, "");
|
|
1949
|
+
url = `${baseUrl}/responses`;
|
|
968
1950
|
if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
|
|
969
1951
|
const runtimeProvider = provider as {
|
|
970
1952
|
_codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
|
|
971
1953
|
_codexAccountRequired?: boolean;
|
|
972
1954
|
};
|
|
973
|
-
if (
|
|
1955
|
+
if (
|
|
1956
|
+
mayForwardCallerCredentials
|
|
1957
|
+
&& runtimeProvider._codexAccountRequired
|
|
1958
|
+
&& !runtimeProvider._codexAccountOverride
|
|
1959
|
+
) {
|
|
974
1960
|
throw new Error("Codex pool account auth is required but unavailable");
|
|
975
1961
|
}
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
|
|
1962
|
+
if (mayForwardCallerCredentials) {
|
|
1963
|
+
for (const h of FORWARD_HEADERS) {
|
|
1964
|
+
const v = incoming?.headers.get(h);
|
|
1965
|
+
if (v) headers[h] = v; // …so forwarded auth always wins.
|
|
1966
|
+
}
|
|
979
1967
|
}
|
|
980
1968
|
const override = runtimeProvider._codexAccountOverride;
|
|
981
|
-
if (override) {
|
|
1969
|
+
if (override && mayForwardCallerCredentials) {
|
|
982
1970
|
headers["authorization"] = `Bearer ${override.accessToken}`;
|
|
983
1971
|
headers["chatgpt-account-id"] = override.chatgptAccountId;
|
|
984
1972
|
}
|
|
985
1973
|
} else {
|
|
986
1974
|
if (provider.responsesPath === undefined) {
|
|
987
|
-
|
|
988
|
-
url = `${base}/v1/responses`;
|
|
1975
|
+
url = openaiResponsesUrl(provider.baseUrl);
|
|
989
1976
|
} else {
|
|
990
1977
|
const base = provider.baseUrl.replace(/\/$/, "");
|
|
991
1978
|
url = `${base}${provider.responsesPath}`;
|
|
@@ -995,11 +1982,18 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
995
1982
|
}
|
|
996
1983
|
|
|
997
1984
|
const forward = provider.authMode === "forward";
|
|
1985
|
+
let convertedRoutedCustomToolNames: Set<string> | undefined;
|
|
1986
|
+
let routedCustomToolRepairNames: Set<string> | undefined;
|
|
1987
|
+
let convertedRoutedToolSearchNames: Set<string> | undefined;
|
|
1988
|
+
let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
|
|
998
1989
|
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
|
|
999
1990
|
let outBody = stripPreviousResponseId(
|
|
1000
1991
|
parsed._rawBody,
|
|
1001
1992
|
forward || parsed._previousResponseInputExpanded === true,
|
|
1002
1993
|
);
|
|
1994
|
+
// stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
|
|
1995
|
+
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
1996
|
+
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
|
|
1003
1997
|
const stateless = provider.statelessResponses === true;
|
|
1004
1998
|
if (stateless) outBody = stripStatefulResponsesParams(outBody);
|
|
1005
1999
|
// A replay miss can leave a function_call_output whose paired function_call sat
|
|
@@ -1008,28 +2002,129 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1008
2002
|
// backend gets — dropping previous_response_id is not much use if the body that
|
|
1009
2003
|
// reaches the wire is unparseable.
|
|
1010
2004
|
if (forward || stateless) {
|
|
1011
|
-
outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
|
|
2005
|
+
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, stateless && !forward);
|
|
2006
|
+
}
|
|
2007
|
+
if (provider.requiresAdjacentResponsesToolResults === true) {
|
|
2008
|
+
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
1012
2009
|
}
|
|
1013
2010
|
if (forward) {
|
|
1014
2011
|
outBody = stripUnsupportedForwardParams(outBody);
|
|
2012
|
+
// Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
|
|
2013
|
+
// third-party forward gateway may still accept it, so this must not be widened.
|
|
2014
|
+
if (isCanonicalOpenAiForwardProvider(provider)) {
|
|
2015
|
+
outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
|
|
2016
|
+
outBody = stripCanonicalForwardPromptCacheOptions(outBody);
|
|
2017
|
+
outBody = normalizeCanonicalForwardPromptEnvelope(outBody);
|
|
2018
|
+
outBody = normalizeCanonicalForwardContinuationEnvelope(outBody);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
outBody = preferConfiguredHostedTools(
|
|
2022
|
+
outBody,
|
|
2023
|
+
provider,
|
|
2024
|
+
parsed.modelId,
|
|
2025
|
+
parsed._openAiVirtualSelectedModelId,
|
|
2026
|
+
);
|
|
2027
|
+
outBody = normalizeImageGenClientTools(outBody);
|
|
1015
2028
|
}
|
|
1016
|
-
else outBody = normalizeImageGenClientTools(outBody);
|
|
1017
2029
|
if (forward || parsed._previousResponseInputExpanded === true) {
|
|
1018
2030
|
outBody = repairOversizedReplayCallIds(outBody);
|
|
1019
2031
|
}
|
|
1020
2032
|
outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
2033
|
+
// Repair stored history from before the bridge emitted both keys: a conversation
|
|
2034
|
+
// that already recorded a single-query web_search_call replays it every turn, and
|
|
2035
|
+
// a strict parser rejects the whole request over it (#930).
|
|
2036
|
+
outBody = backfillWebSearchQueries(outBody);
|
|
2037
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
2038
|
+
outBody = promoteClientLoadedTools(outBody);
|
|
2039
|
+
}
|
|
2040
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
2041
|
+
const rewritten = rewriteRoutedCustomToolsForUpstream(
|
|
2042
|
+
outBody,
|
|
2043
|
+
provider.supportsResponsesCustomTools,
|
|
2044
|
+
);
|
|
2045
|
+
outBody = rewritten.body;
|
|
2046
|
+
convertedRoutedCustomToolNames = rewritten.names;
|
|
2047
|
+
routedCustomToolRepairNames = rewritten.repairNames;
|
|
2048
|
+
}
|
|
2049
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
2050
|
+
// Run after custom-tool lowering so the search compatibility layer can choose a
|
|
2051
|
+
// collision-free public function name against the final routed function catalog.
|
|
2052
|
+
const rewritten = rewriteRoutedToolSearchForUpstream(outBody);
|
|
2053
|
+
outBody = rewritten.body;
|
|
2054
|
+
convertedRoutedToolSearchNames = rewritten.names;
|
|
2055
|
+
}
|
|
2056
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
2057
|
+
// Codex 0.147 emits private namespace tool groups, while public/third-party Responses
|
|
2058
|
+
// gateways accept only flat tool variants. Run after custom/tool-search lowering so
|
|
2059
|
+
// namespace children already carry their final public kind before they are promoted.
|
|
2060
|
+
const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody);
|
|
2061
|
+
outBody = rewritten.body;
|
|
2062
|
+
convertedRoutedNamespaceToolAliases = rewritten.aliases;
|
|
2063
|
+
// Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
|
|
2064
|
+
// generic capability fallback removes the private OpenAI fields.
|
|
2065
|
+
outBody = normalizeXaiResponsesWebSearch(outBody, provider);
|
|
2066
|
+
// xAI and explicitly classified compatible gateways reject these OpenAI web_search
|
|
2067
|
+
// extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
|
|
2068
|
+
if (provider.supportsOpenAiWebSearchToolFields === false) {
|
|
2069
|
+
outBody = stripOpenAiOnlyWebSearchFields(outBody);
|
|
2070
|
+
}
|
|
2071
|
+
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId);
|
|
2072
|
+
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
2073
|
+
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
2074
|
+
}
|
|
2075
|
+
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
|
|
2076
|
+
// let a noncanonical custom forward provider skip this rewrite while the server still routes
|
|
2077
|
+
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
|
|
2078
|
+
// therefore be the last routed transform: anything before it may depend on the declarations;
|
|
2079
|
+
// anything after it cannot.
|
|
1024
2080
|
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
1025
2081
|
outBody = buildRoutedCompactionBody(outBody);
|
|
1026
2082
|
}
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
|
|
2083
|
+
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
|
|
2084
|
+
const sanitizedBody = normalizeToolSchemas(
|
|
2085
|
+
stripSparkCompatibility(
|
|
2086
|
+
stripUnsupportedReasoningParams(
|
|
2087
|
+
stripItemIdsWhenUnstored(
|
|
2088
|
+
stripInvalidItemIds(
|
|
2089
|
+
stripUnsupportedHostedTools(
|
|
2090
|
+
sanitizeReasoningInputContent(
|
|
2091
|
+
scrubOcxCompactionItems(
|
|
2092
|
+
outBody,
|
|
2093
|
+
destinationDecodesNativeCompactionBlob(provider),
|
|
2094
|
+
threadServingIdentityChanged,
|
|
2095
|
+
),
|
|
2096
|
+
{
|
|
2097
|
+
preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
|
|
2098
|
+
dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
|
|
2099
|
+
stripEncryptedContent: threadServingIdentityChanged,
|
|
2100
|
+
},
|
|
2101
|
+
),
|
|
2102
|
+
),
|
|
2103
|
+
),
|
|
2104
|
+
),
|
|
2105
|
+
),
|
|
2106
|
+
),
|
|
2107
|
+
isXaiSchemaTarget(provider),
|
|
2108
|
+
);
|
|
2109
|
+
const finalBody = stripDisabledVerbosity(
|
|
2110
|
+
stripDisabledReasoningSummaries(
|
|
2111
|
+
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
|
|
2112
|
+
provider,
|
|
2113
|
+
parsed.modelId,
|
|
2114
|
+
),
|
|
1030
2115
|
provider,
|
|
1031
2116
|
parsed.modelId,
|
|
1032
|
-
)
|
|
2117
|
+
);
|
|
2118
|
+
const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
|
|
2119
|
+
? finalBody.service_tier
|
|
2120
|
+
: null;
|
|
2121
|
+
const tierLog = createAdapterTierMetadata(
|
|
2122
|
+
parsed.options?.tierObservation,
|
|
2123
|
+
parsed.options?.tierDecision,
|
|
2124
|
+
actualServiceTier === null ? null : "service-tier",
|
|
2125
|
+
actualServiceTier,
|
|
2126
|
+
);
|
|
2127
|
+
const body = JSON.stringify(finalBody);
|
|
1033
2128
|
const releaseBodyObservation = translatorBudget.observeExternallyCapped(
|
|
1034
2129
|
"passthrough_serialization",
|
|
1035
2130
|
new TextEncoder().encode(body).byteLength,
|
|
@@ -1040,6 +2135,11 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1040
2135
|
headers,
|
|
1041
2136
|
body,
|
|
1042
2137
|
releaseBodyObservation,
|
|
2138
|
+
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
|
|
2139
|
+
...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
|
|
2140
|
+
...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
|
|
2141
|
+
...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
|
|
2142
|
+
...(tierLog ? { tierLog } : {}),
|
|
1043
2143
|
};
|
|
1044
2144
|
},
|
|
1045
2145
|
|