@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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 +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Bounded classification of a PARTIAL freeform tool-call wrapper.
|
|
2
|
+
//
|
|
3
|
+
// `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
|
|
4
|
+
// what order an object's properties arrive in or how their names are spelled. The streaming side
|
|
5
|
+
// has to reach the same answer from a prefix, and it used to do that by comparing the buffer
|
|
6
|
+
// against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
|
|
7
|
+
// wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
|
|
8
|
+
// `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
|
|
9
|
+
// completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
|
|
10
|
+
// same disagreement; reordering and escaping are the two the literal matcher could never see,
|
|
11
|
+
// because RFC 8259 objects are unordered and their names are strings with escapes.
|
|
12
|
+
//
|
|
13
|
+
// So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
|
|
14
|
+
// which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
|
|
15
|
+
//
|
|
16
|
+
// `input` the canonical key is present with a string value, whatever preceded it and however
|
|
17
|
+
// its name was spelled. Its value can be decoded progressively, because
|
|
18
|
+
// `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
|
|
19
|
+
// `raw` no wrapper can apply, because the text is not an object or because it is one that
|
|
20
|
+
// `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
|
|
21
|
+
// `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
|
|
22
|
+
// is published until the object parses and completion's own rule decides.
|
|
23
|
+
//
|
|
24
|
+
// The bound matters as much as the classification. A scan that walks the whole buffer on every
|
|
25
|
+
// delta is quadratic in the argument size, so classification gives up after
|
|
26
|
+
// `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
|
|
27
|
+
// buffer is still resolved by the authoritative parse at completion.
|
|
28
|
+
|
|
29
|
+
/** The insignificant whitespace `JSON.parse` accepts between tokens. */
|
|
30
|
+
export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
31
|
+
|
|
32
|
+
/** The two-character escapes JSON defines, and nothing else. */
|
|
33
|
+
export const JSON_ESCAPES = new Map<string, string>([
|
|
34
|
+
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
35
|
+
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How far a classification scan walks before it gives up and holds.
|
|
40
|
+
*
|
|
41
|
+
* A wrapper's own structure — its property names and the small scalar siblings a model puts
|
|
42
|
+
* beside the body — lives at the front of the object. A buffer still undecided after this much
|
|
43
|
+
* scanning is one whose preceding values are large, and holding it is what the routed
|
|
44
|
+
* restoration path already does for every unrecognized object.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
|
|
47
|
+
|
|
48
|
+
const CANONICAL_KEY = "input";
|
|
49
|
+
|
|
50
|
+
/** The buffer ends inside a token: what follows can still change the answer. */
|
|
51
|
+
const HOLD = -1;
|
|
52
|
+
/** `JSON.parse` will reject this text no matter what is appended to it. */
|
|
53
|
+
const NEVER = -2;
|
|
54
|
+
|
|
55
|
+
export type FreeformWrapperScan =
|
|
56
|
+
| { kind: "hold"; parse: boolean }
|
|
57
|
+
| { kind: "raw" }
|
|
58
|
+
| { kind: "input"; valueStart: number };
|
|
59
|
+
|
|
60
|
+
/** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
|
|
61
|
+
function skipWhitespace(text: string, from: number): number {
|
|
62
|
+
let i = from;
|
|
63
|
+
while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
|
|
64
|
+
return i < text.length ? i : HOLD;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
|
|
69
|
+
*
|
|
70
|
+
* This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
|
|
71
|
+
* fail on a truncated string and the other has to return what it decoded so far. They answer
|
|
72
|
+
* different questions about the same bytes and share one escape table so they cannot disagree
|
|
73
|
+
* about which escapes exist.
|
|
74
|
+
*/
|
|
75
|
+
function scanString(text: string, from: number): number {
|
|
76
|
+
let i = from + 1;
|
|
77
|
+
while (i < text.length) {
|
|
78
|
+
const c = text[i]!;
|
|
79
|
+
if (c === '"') return i + 1;
|
|
80
|
+
if (c === "\\") {
|
|
81
|
+
const n = text[i + 1];
|
|
82
|
+
if (n === undefined) return HOLD;
|
|
83
|
+
if (n === "u") {
|
|
84
|
+
const hex = text.slice(i + 2, i + 6);
|
|
85
|
+
if (hex.length < 4) return HOLD;
|
|
86
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
|
|
87
|
+
i += 6;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!JSON_ESCAPES.has(n)) return NEVER;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// A literal control character is not legal inside a JSON string.
|
|
95
|
+
if (c.charCodeAt(0) <= 0x1f) return NEVER;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return HOLD;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
|
|
102
|
+
const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
103
|
+
|
|
104
|
+
/** Index just past one complete scalar at `from`, or HOLD / NEVER. */
|
|
105
|
+
function scanScalar(text: string, from: number): number {
|
|
106
|
+
const c = text[from]!;
|
|
107
|
+
if (c === '"') return scanString(text, from);
|
|
108
|
+
if (c === "t" || c === "f" || c === "n") {
|
|
109
|
+
const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
|
|
110
|
+
if (text.startsWith(literal, from)) return from + literal.length;
|
|
111
|
+
return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
|
|
112
|
+
}
|
|
113
|
+
let end = from;
|
|
114
|
+
while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
|
|
115
|
+
if (end === from) return NEVER;
|
|
116
|
+
// A number running to the end of the buffer can still grow another digit or exponent.
|
|
117
|
+
if (end >= text.length) return HOLD;
|
|
118
|
+
return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
|
|
122
|
+
function scanMemberKey(text: string, from: number): number {
|
|
123
|
+
if (text[from] !== '"') return NEVER;
|
|
124
|
+
const nameEnd = scanString(text, from);
|
|
125
|
+
if (nameEnd < 0) return nameEnd;
|
|
126
|
+
const colon = skipWhitespace(text, nameEnd);
|
|
127
|
+
if (colon === HOLD) return HOLD;
|
|
128
|
+
if (text[colon] !== ":") return NEVER;
|
|
129
|
+
return skipWhitespace(text, colon + 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Index just past one complete JSON value at `from`, or HOLD / NEVER.
|
|
134
|
+
*
|
|
135
|
+
* Containers are walked with an explicit stack rather than recursion: the value being skipped
|
|
136
|
+
* is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
|
|
137
|
+
*/
|
|
138
|
+
function scanValue(text: string, from: number): number {
|
|
139
|
+
const closers: string[] = [];
|
|
140
|
+
let i = from;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const c = text[i]!;
|
|
143
|
+
if (c === "{" || c === "[") {
|
|
144
|
+
const closer = c === "{" ? "}" : "]";
|
|
145
|
+
closers.push(closer);
|
|
146
|
+
const first = skipWhitespace(text, i + 1);
|
|
147
|
+
if (first === HOLD) return HOLD;
|
|
148
|
+
if (text[first] === closer) {
|
|
149
|
+
closers.pop();
|
|
150
|
+
i = first + 1;
|
|
151
|
+
} else if (closer === "}") {
|
|
152
|
+
const value = scanMemberKey(text, first);
|
|
153
|
+
if (value < 0) return value;
|
|
154
|
+
i = value;
|
|
155
|
+
continue;
|
|
156
|
+
} else {
|
|
157
|
+
i = first;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
const end = scanScalar(text, i);
|
|
162
|
+
if (end < 0) return end;
|
|
163
|
+
i = end;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One value is complete: close whatever it finished and find the next value, if any.
|
|
167
|
+
for (;;) {
|
|
168
|
+
if (closers.length === 0) return i;
|
|
169
|
+
const at = skipWhitespace(text, i);
|
|
170
|
+
if (at === HOLD) return HOLD;
|
|
171
|
+
const closer = closers[closers.length - 1]!;
|
|
172
|
+
if (text[at] === closer) {
|
|
173
|
+
closers.pop();
|
|
174
|
+
i = at + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (text[at] !== ",") return NEVER;
|
|
178
|
+
const next = skipWhitespace(text, at + 1);
|
|
179
|
+
if (next === HOLD) return HOLD;
|
|
180
|
+
if (closer === "}") {
|
|
181
|
+
const value = scanMemberKey(text, next);
|
|
182
|
+
if (value < 0) return value;
|
|
183
|
+
i = value;
|
|
184
|
+
} else {
|
|
185
|
+
i = next;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which wrapper the completed text will unwrap to, as far as this prefix can say.
|
|
194
|
+
*
|
|
195
|
+
* Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
|
|
196
|
+
* carries a string, and a second one can still arrive, so no prefix decides them — which makes
|
|
197
|
+
* them indistinguishable from any other undecided object and lets one HOLD cover both.
|
|
198
|
+
*/
|
|
199
|
+
export function scanFreeformWrapper(text: string): FreeformWrapperScan {
|
|
200
|
+
// One clamp rather than a budget threaded through every helper. Every helper already holds
|
|
201
|
+
// when it runs off the end of what it can see, so a buffer whose classification needs more
|
|
202
|
+
// than this holds for exactly the right reason, and no scan can cost more than this many
|
|
203
|
+
// characters however large the arguments grow. Indices into the clamp are indices into the
|
|
204
|
+
// full text, because the clamp is a prefix of it.
|
|
205
|
+
const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
|
|
206
|
+
? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
|
|
207
|
+
: text;
|
|
208
|
+
// `parse` is true only where this scan actually SAW the object close. Every other hold ran
|
|
209
|
+
// out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
|
|
210
|
+
// possible payoff: the first is provably incomplete, and the second would re-read a growing
|
|
211
|
+
// buffer on every delta that happens to end in a brace — repeated braces inside a long
|
|
212
|
+
// unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
|
|
213
|
+
// costs nothing that matters, because the value it would release arrives in the same instant
|
|
214
|
+
// as the authoritative completion that follows it.
|
|
215
|
+
const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
|
|
216
|
+
const open = skipWhitespace(bounded, 0);
|
|
217
|
+
if (open === HOLD) return hold();
|
|
218
|
+
// Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
|
|
219
|
+
// complete as themselves.
|
|
220
|
+
if (bounded[open] !== "{") return { kind: "raw" };
|
|
221
|
+
|
|
222
|
+
let i = open + 1;
|
|
223
|
+
for (;;) {
|
|
224
|
+
const at = skipWhitespace(bounded, i);
|
|
225
|
+
if (at === HOLD) return hold();
|
|
226
|
+
if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
|
|
227
|
+
if (bounded[at] !== '"') return { kind: "raw" };
|
|
228
|
+
|
|
229
|
+
const nameEnd = scanString(bounded, at);
|
|
230
|
+
if (nameEnd === HOLD) return hold();
|
|
231
|
+
if (nameEnd === NEVER) return { kind: "raw" };
|
|
232
|
+
let name: string;
|
|
233
|
+
try {
|
|
234
|
+
// The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
|
|
235
|
+
// key completion sees. Decoding it by hand here is the second implementation that would
|
|
236
|
+
// drift from `JSON.parse`, which is how this defect existed in the first place.
|
|
237
|
+
name = JSON.parse(bounded.slice(at, nameEnd)) as string;
|
|
238
|
+
} catch {
|
|
239
|
+
return { kind: "raw" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const colon = skipWhitespace(bounded, nameEnd);
|
|
243
|
+
if (colon === HOLD) return hold();
|
|
244
|
+
if (bounded[colon] !== ":") return { kind: "raw" };
|
|
245
|
+
const valueAt = skipWhitespace(bounded, colon + 1);
|
|
246
|
+
if (valueAt === HOLD) return hold();
|
|
247
|
+
|
|
248
|
+
if (name === CANONICAL_KEY) {
|
|
249
|
+
// An own `input` wins over everything else in the object, so this is decidable now. A
|
|
250
|
+
// non-string value is decidable too, in the other direction: completion hands back the
|
|
251
|
+
// argument text unchanged rather than unwrapping a value it cannot use.
|
|
252
|
+
return bounded[valueAt] === '"'
|
|
253
|
+
? { kind: "input", valueStart: valueAt + 1 }
|
|
254
|
+
: { kind: "raw" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const valueEnd = scanValue(bounded, valueAt);
|
|
258
|
+
if (valueEnd === HOLD) return hold();
|
|
259
|
+
if (valueEnd === NEVER) return { kind: "raw" };
|
|
260
|
+
|
|
261
|
+
const next = skipWhitespace(bounded, valueEnd);
|
|
262
|
+
if (next === HOLD) return hold();
|
|
263
|
+
if (bounded[next] === ",") {
|
|
264
|
+
i = next + 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
|
|
268
|
+
return { kind: "raw" };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The object closed without a canonical key. Only whitespace may follow one that parses, so
|
|
274
|
+
* anything else makes the text raw; otherwise the completed parse decides between a fallback
|
|
275
|
+
* wrapper and no wrapper at all.
|
|
276
|
+
*/
|
|
277
|
+
function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
|
|
278
|
+
return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
|
|
279
|
+
}
|
|
@@ -33,6 +33,38 @@ function namespaceOf(value: unknown): string | undefined {
|
|
|
33
33
|
return typeof value === "string" && value !== "functions" ? value : undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Repair one provider spelling drift for Codex's flat shell bridge.
|
|
38
|
+
*
|
|
39
|
+
* This is schema-bound rather than a general alias: `input` is also a legitimate
|
|
40
|
+
* argument name for arbitrary caller and MCP tools. Only the exact bare
|
|
41
|
+
* `exec_command` declaration can establish that its sole string input has one faithful
|
|
42
|
+
* reading as the required string `cmd` member.
|
|
43
|
+
*/
|
|
44
|
+
function repairExecCommandInput(
|
|
45
|
+
argumentsText: string,
|
|
46
|
+
schema: FunctionCallRepairSchema,
|
|
47
|
+
): string {
|
|
48
|
+
if (schema.namespace !== undefined || schema.name !== "exec_command") return argumentsText;
|
|
49
|
+
const parameters = schema.parameters;
|
|
50
|
+
if (!isObject(parameters) || parameters.type !== "object"
|
|
51
|
+
|| !Array.isArray(parameters.required) || !parameters.required.includes("cmd")
|
|
52
|
+
|| !isObject(parameters.properties) || !isObject(parameters.properties.cmd)
|
|
53
|
+
|| parameters.properties.cmd.type !== "string") return argumentsText;
|
|
54
|
+
let parsed: unknown;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(argumentsText);
|
|
57
|
+
} catch {
|
|
58
|
+
return argumentsText;
|
|
59
|
+
}
|
|
60
|
+
if (!isObject(parsed)) return argumentsText;
|
|
61
|
+
const keys = Object.keys(parsed);
|
|
62
|
+
if (keys.length !== 1 || keys[0] !== "input" || typeof parsed.input !== "string") {
|
|
63
|
+
return argumentsText;
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify({ cmd: parsed.input });
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
function selectorAllows(
|
|
37
69
|
selector: unknown,
|
|
38
70
|
lowered: unknown,
|
|
@@ -131,7 +163,12 @@ function repairItem(item: unknown, schemas: FunctionCallRepairSchemas, completed
|
|
|
131
163
|
if (unsafe) return item;
|
|
132
164
|
} catch { return item; }
|
|
133
165
|
}
|
|
134
|
-
const
|
|
166
|
+
const integerRepaired = coerceIntegerToolArguments(
|
|
167
|
+
raw || "{}",
|
|
168
|
+
schema.parameters,
|
|
169
|
+
schema.namespace ? undefined : schema.name,
|
|
170
|
+
);
|
|
171
|
+
const argumentsText = repairExecCommandInput(integerRepaired, schema);
|
|
135
172
|
return argumentsText === raw ? item : { ...item, arguments: argumentsText };
|
|
136
173
|
}
|
|
137
174
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { OcxDocumentContent } from "../types";
|
|
2
|
+
|
|
3
|
+
const DATA_URL = /^data:([^;,]+)((?:;[^;,]*)*),(.*)$/s;
|
|
4
|
+
const BASE64_PAYLOAD = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
5
|
+
|
|
6
|
+
function usableBase64(payload: string): boolean {
|
|
7
|
+
if (!BASE64_PAYLOAD.test(payload)) return false;
|
|
8
|
+
// Preserve valid unpadded encodings, but a single sextet cannot represent any byte.
|
|
9
|
+
// Padding, when present, must complete a four-character quantum. No payload allocation.
|
|
10
|
+
return payload.endsWith("=") ? payload.length % 4 === 0 : payload.length % 4 !== 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The one marker vocabulary for an attached document, derived from what the part knows. */
|
|
14
|
+
export function inlineDocumentMarker(filename: string | undefined): string {
|
|
15
|
+
return filename !== undefined && filename.length > 0 ? `[document: ${filename}]` : "[document]";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An inline document part from a `data:` URL, or `undefined` when there are no usable bytes.
|
|
20
|
+
*
|
|
21
|
+
* A reference with no payload — a `file_id`, a bare filename — is deliberately not a document:
|
|
22
|
+
* there is nothing to carry, and minting a part for it would claim an attachment the request
|
|
23
|
+
* never contained. Those keep the marker path they already had.
|
|
24
|
+
*/
|
|
25
|
+
export function inlineDocumentFromDataUrl(
|
|
26
|
+
fileData: string | undefined,
|
|
27
|
+
filename: string | undefined,
|
|
28
|
+
): OcxDocumentContent | undefined {
|
|
29
|
+
if (typeof fileData !== "string" || fileData.length === 0) return undefined;
|
|
30
|
+
const match = DATA_URL.exec(fileData);
|
|
31
|
+
if (!match) return undefined;
|
|
32
|
+
const mediaType = match[1]!;
|
|
33
|
+
const payload = match[3]!;
|
|
34
|
+
if (!hasBase64Parameter(match[2] ?? "") || !usableBase64(payload)) return undefined;
|
|
35
|
+
return {
|
|
36
|
+
type: "document",
|
|
37
|
+
text: inlineDocumentMarker(filename),
|
|
38
|
+
mediaType,
|
|
39
|
+
data: payload,
|
|
40
|
+
...(filename !== undefined && filename.length > 0 ? { filename } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The `data:` URL spelling of a document part, for wires whose counterpart takes one. */
|
|
45
|
+
export function inlineDocumentDataUrl(part: OcxDocumentContent): string {
|
|
46
|
+
return `data:${part.mediaType};base64,${part.data}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether this string is a `data:` URL this module would actually decode.
|
|
51
|
+
*
|
|
52
|
+
* The media-type scanner and this parser have to agree exactly. A looser scanner would exempt an
|
|
53
|
+
* attachment from the untranslated-media refusal that the parser then reduces to a marker, which
|
|
54
|
+
* is the silent drop the refusal exists to prevent. `;base64` must be a whole parameter token,
|
|
55
|
+
* not a substring of one: `;notbase64,` and `;x=base64,` are not base64 payloads.
|
|
56
|
+
*/
|
|
57
|
+
export function isInlineDocumentDataUrl(value: unknown): boolean {
|
|
58
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
59
|
+
const match = DATA_URL.exec(value);
|
|
60
|
+
return match !== null && hasBase64Parameter(match[2] ?? "") && usableBase64(match[3]!);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasBase64Parameter(parameters: string): boolean {
|
|
64
|
+
return parameters.split(";").includes("base64");
|
|
65
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isInlineDocumentDataUrl } from "./inline-document";
|
|
2
|
+
|
|
1
3
|
/** Input kinds for which the normalized request has no lossless content carrier. */
|
|
2
4
|
export type UntranslatedInputMedia = "audio" | "file";
|
|
3
5
|
|
|
@@ -7,20 +9,47 @@ function isRecord(value: unknown): value is RecordValue {
|
|
|
7
9
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether the position being scanned has a converter that builds a document part.
|
|
14
|
+
*
|
|
15
|
+
* Only user content does. A tool output, a system or assistant message, and a Chat `developer`
|
|
16
|
+
* message are all flattened to text by their converters, so exempting an attachment there would
|
|
17
|
+
* turn today's explicit refusal into the silent drop this scanner exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
type CarrierPosition = "user-content" | "flattened";
|
|
20
|
+
|
|
21
|
+
function mediaKind(value: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
|
|
11
22
|
if (!isRecord(value)) return undefined;
|
|
12
23
|
if (value.type === "input_audio" || value.type === "audio") return "audio";
|
|
13
|
-
if (value.type === "input_file" || value.type === "file" || value.type === "document")
|
|
24
|
+
if (value.type === "input_file" || value.type === "file" || value.type === "document") {
|
|
25
|
+
// A document that carries its own bytes has a lossless carrier in user content, so refusing
|
|
26
|
+
// it there would reject the very request #5212 exists to preserve. A reference with no
|
|
27
|
+
// payload has none anywhere: translated adapters cannot dereference a file_id or a remote
|
|
28
|
+
// source.
|
|
29
|
+
return position === "user-content" && carriesInlineDocumentBytes(value) ? undefined : "file";
|
|
30
|
+
}
|
|
14
31
|
// A file-id-only image is not pixels: translated adapters cannot dereference it.
|
|
15
32
|
if (value.type === "input_image" && typeof value.file_id === "string" && value.file_id.length > 0
|
|
16
33
|
&& !(typeof value.image_url === "string" && value.image_url.length > 0)) return "file";
|
|
17
34
|
return undefined;
|
|
18
35
|
}
|
|
19
36
|
|
|
20
|
-
|
|
37
|
+
/** Presence of a base64 payload only. No payload is read, decoded, copied or returned. */
|
|
38
|
+
function carriesInlineDocumentBytes(value: RecordValue): boolean {
|
|
39
|
+
if (isInlineDocumentDataUrl(value.file_data)) return true;
|
|
40
|
+
// Chat Completions nests the payload under `file`.
|
|
41
|
+
if (isRecord(value.file) && isInlineDocumentDataUrl(value.file.file_data)) return true;
|
|
42
|
+
// Anthropic nests it under a base64 `source`.
|
|
43
|
+
return isRecord(value.source)
|
|
44
|
+
&& value.source.type === "base64"
|
|
45
|
+
&& typeof value.source.data === "string"
|
|
46
|
+
&& value.source.data.length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function contentMedia(content: unknown, position: CarrierPosition): UntranslatedInputMedia | undefined {
|
|
21
50
|
if (!Array.isArray(content)) return undefined;
|
|
22
51
|
for (const part of content) {
|
|
23
|
-
const kind = mediaKind(part);
|
|
52
|
+
const kind = mediaKind(part, position);
|
|
24
53
|
if (kind) return kind;
|
|
25
54
|
}
|
|
26
55
|
return undefined;
|
|
@@ -35,13 +64,16 @@ export function untranslatedResponsesInputMedia(body: unknown): UntranslatedInpu
|
|
|
35
64
|
if (!isRecord(body) || !Array.isArray(body.input)) return undefined;
|
|
36
65
|
for (const item of body.input) {
|
|
37
66
|
if (!isRecord(item)) continue;
|
|
38
|
-
const direct = mediaKind(item);
|
|
67
|
+
const direct = mediaKind(item, "flattened");
|
|
39
68
|
if (direct) return direct;
|
|
40
69
|
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
41
|
-
const kind = contentMedia(item.output);
|
|
70
|
+
const kind = contentMedia(item.output, "flattened");
|
|
42
71
|
if (kind) return kind;
|
|
43
72
|
} else if (item.type === "message" || item.type === undefined) {
|
|
44
|
-
|
|
73
|
+
// `inputContentParts` runs for user and developer messages only; a system message is
|
|
74
|
+
// flattened to text by the parser.
|
|
75
|
+
const role = item.role;
|
|
76
|
+
const kind = contentMedia(item.content, role === "user" || role === "developer" ? "user-content" : "flattened");
|
|
45
77
|
if (kind) return kind;
|
|
46
78
|
}
|
|
47
79
|
}
|
|
@@ -53,7 +85,9 @@ export function untranslatedChatInputMedia(body: unknown): UntranslatedInputMedi
|
|
|
53
85
|
if (!isRecord(body) || !Array.isArray(body.messages)) return undefined;
|
|
54
86
|
for (const message of body.messages) {
|
|
55
87
|
if (!isRecord(message)) continue;
|
|
56
|
-
|
|
88
|
+
// Only the `user` branch of the Chat projection builds content blocks. `system`,
|
|
89
|
+
// `developer`, `assistant` and `tool` all reduce their content to a string.
|
|
90
|
+
const kind = contentMedia(message.content, message.role === "user" ? "user-content" : "flattened");
|
|
57
91
|
if (kind) return kind;
|
|
58
92
|
}
|
|
59
93
|
return undefined;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { collectResponsesToolGroups } from "./tool-groups";
|
|
2
|
+
import { isSchemaValidResponsesToolName, wireToolInnerName } from "./tool-name-aliases";
|
|
3
|
+
import { CODE_MODE_HELPER_WIRE_NAMES, dottedToolName, namespacedToolName } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one invented namespace this proxy is known to have emitted. Routed providers flatten a
|
|
7
|
+
* namespaced call as `ns.name`, and a provider that invents the grouping picks `default`; #3402,
|
|
8
|
+
* #4176 and #4412 all repaired that spelling on the way IN, each on its own path.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately one literal prefix rather than a rule. "Strip everything before the first
|
|
11
|
+
* dot" would silently rewrite a legitimate tool name that happens to contain a dot in some other
|
|
12
|
+
* provider's vocabulary, and a replayed history item is the worst possible place to guess: it names
|
|
13
|
+
* a call that already happened.
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_INVENTED_NAMESPACE_PREFIX = "default.";
|
|
16
|
+
|
|
17
|
+
/** Item types whose `name` the upstream validates against the tool-name pattern. */
|
|
18
|
+
const NAMED_CALL_ITEM_TYPES = new Set(["function_call", "custom_tool_call"]);
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DeclarationView = {
|
|
25
|
+
/** Wire spellings the caller's own catalog declares, including flattened namespace forms. */
|
|
26
|
+
readonly spellings: ReadonlySet<string>;
|
|
27
|
+
/** How many declared identities answer to a given inner name. */
|
|
28
|
+
readonly ownersByInnerName: ReadonlyMap<string, number>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the request's own catalog. Namespace children contribute their inner name and both
|
|
33
|
+
* flattened spellings, which is what makes a declared `default` namespace distinguishable from an
|
|
34
|
+
* invented one.
|
|
35
|
+
*/
|
|
36
|
+
function readDeclarations(body: unknown): DeclarationView {
|
|
37
|
+
const spellings = new Set<string>();
|
|
38
|
+
const owners = new Map<string, number>();
|
|
39
|
+
const identities = new Map<string, Set<string>>();
|
|
40
|
+
const claim = (innerName: string, identity: string): void => {
|
|
41
|
+
const seen = identities.get(innerName);
|
|
42
|
+
if (seen === undefined) identities.set(innerName, new Set([identity]));
|
|
43
|
+
else seen.add(identity);
|
|
44
|
+
};
|
|
45
|
+
for (const group of collectResponsesToolGroups(body)) {
|
|
46
|
+
for (const spec of group) {
|
|
47
|
+
if (!isPlainObject(spec)) continue;
|
|
48
|
+
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
49
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
50
|
+
if (namespace === undefined) continue;
|
|
51
|
+
for (const child of spec.tools) {
|
|
52
|
+
const inner = wireToolInnerName(child);
|
|
53
|
+
if (!inner) continue;
|
|
54
|
+
spellings.add(namespacedToolName(namespace, inner));
|
|
55
|
+
spellings.add(dottedToolName(namespace, inner));
|
|
56
|
+
claim(inner, JSON.stringify([namespace, inner]));
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const name = wireToolInnerName(spec);
|
|
61
|
+
if (!name) continue;
|
|
62
|
+
spellings.add(name);
|
|
63
|
+
claim(name, JSON.stringify([null, name]));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [inner, owning] of identities) owners.set(inner, owning.size);
|
|
67
|
+
return { spellings, ownersByInnerName: owners };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Canonical name a legacy dotted alias should be replayed under, or undefined to leave it alone.
|
|
72
|
+
*
|
|
73
|
+
* Fail-closed in both directions that matter. A dotted spelling the caller's catalog actually
|
|
74
|
+
* declares is a real tool identity, so it is never stripped — rewriting it would change which tool
|
|
75
|
+
* the history says was called. A suffix claimed by two declared identities is ambiguous and is
|
|
76
|
+
* left as it is. Only two things resolve: a suffix that names exactly one declared tool, and the
|
|
77
|
+
* bounded set of code-mode helper spellings, which a code-mode catalog never declares because they
|
|
78
|
+
* exist only as nested `tools.*` helpers inside `exec`.
|
|
79
|
+
*/
|
|
80
|
+
function repairedCallName(name: string, declarations: DeclarationView): string | undefined {
|
|
81
|
+
if (isSchemaValidResponsesToolName(name)) return undefined;
|
|
82
|
+
if (!name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)) return undefined;
|
|
83
|
+
if (declarations.spellings.has(name)) return undefined;
|
|
84
|
+
const suffix = name.slice(LEGACY_INVENTED_NAMESPACE_PREFIX.length);
|
|
85
|
+
if (!isSchemaValidResponsesToolName(suffix)) return undefined;
|
|
86
|
+
const owners = declarations.ownersByInnerName.get(suffix);
|
|
87
|
+
if (owners === 1) return suffix;
|
|
88
|
+
if (owners !== undefined) return undefined;
|
|
89
|
+
return CODE_MODE_HELPER_WIRE_NAMES.has(suffix) ? suffix : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function repairedItem(item: unknown, declarations: DeclarationView): unknown {
|
|
93
|
+
if (!isPlainObject(item)) return item;
|
|
94
|
+
if (typeof item.type !== "string" || !NAMED_CALL_ITEM_TYPES.has(item.type)) return item;
|
|
95
|
+
// An item carrying an explicit namespace is a namespaced identity whose `name` is the inner
|
|
96
|
+
// name; it is not the flattened shape this repairs, and its own field is the authority.
|
|
97
|
+
if (item.namespace !== undefined) return item;
|
|
98
|
+
if (typeof item.name !== "string") return item;
|
|
99
|
+
const repaired = repairedCallName(item.name, declarations);
|
|
100
|
+
return repaired === undefined ? item : { ...item, name: repaired };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Make an already-damaged conversation replayable.
|
|
105
|
+
*
|
|
106
|
+
* A `function_call` whose name violates `^[a-zA-Z0-9_-]+$` is not a one-turn failure: Codex stores
|
|
107
|
+
* the item, and the upstream then refuses EVERY later request that replays it
|
|
108
|
+
* (`Invalid 'input[N].name': ...`) — including the automatic compaction a long task or a side chat
|
|
109
|
+
* triggers. The emit-side fix stops new items from being created; without this the conversations
|
|
110
|
+
* that already contain one stay permanently unusable, which is the part of #5095 that actually
|
|
111
|
+
* hurts.
|
|
112
|
+
*
|
|
113
|
+
* Only replayed input items are touched. The caller's tool catalog is never rewritten, and a body
|
|
114
|
+
* with nothing to repair is returned by reference.
|
|
115
|
+
*/
|
|
116
|
+
export function repairLegacyDottedToolCallNames(body: unknown): unknown {
|
|
117
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
118
|
+
if (!body.input.some(item =>
|
|
119
|
+
isPlainObject(item)
|
|
120
|
+
&& typeof item.type === "string"
|
|
121
|
+
&& NAMED_CALL_ITEM_TYPES.has(item.type)
|
|
122
|
+
&& typeof item.name === "string"
|
|
123
|
+
&& item.name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)
|
|
124
|
+
)) return body;
|
|
125
|
+
|
|
126
|
+
const declarations = readDeclarations(body);
|
|
127
|
+
let changed = false;
|
|
128
|
+
const input = body.input.map(item => {
|
|
129
|
+
const repaired = repairedItem(item, declarations);
|
|
130
|
+
if (repaired !== item) changed = true;
|
|
131
|
+
return repaired;
|
|
132
|
+
});
|
|
133
|
+
return changed ? { ...body, input } : body;
|
|
134
|
+
}
|
|
@@ -8,6 +8,25 @@ const MUSE_SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
|
8
8
|
|
|
9
9
|
export type MuseToolNameAliases = ReadonlyMap<string, string>;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Final wire name for an identity before the Muse length rewrite.
|
|
13
|
+
*
|
|
14
|
+
* The alias map is request-local and already narrowed by `tool_choice`. Returning undefined for
|
|
15
|
+
* a contradictory map keeps downstream identity checks fail-closed instead of choosing one alias.
|
|
16
|
+
*/
|
|
17
|
+
export function museWireNameForOriginal(
|
|
18
|
+
originalName: string,
|
|
19
|
+
aliases: MuseToolNameAliases,
|
|
20
|
+
): string | undefined {
|
|
21
|
+
let wireName: string | undefined;
|
|
22
|
+
for (const [candidate, original] of aliases) {
|
|
23
|
+
if (original !== originalName) continue;
|
|
24
|
+
if (wireName !== undefined && wireName !== candidate) return undefined;
|
|
25
|
+
wireName = candidate;
|
|
26
|
+
}
|
|
27
|
+
return wireName ?? originalName;
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
12
31
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
13
32
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxContentPart, OcxTextContent } from "../types";
|
|
2
|
+
import { inlineDocumentFromDataUrl } from "./inline-document";
|
|
2
3
|
|
|
3
4
|
export function isObj(v: unknown): v is Record<string, unknown> {
|
|
4
5
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -90,8 +91,13 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
90
91
|
if (fileId) {
|
|
91
92
|
parts.push({ type: "text", text: `[file: ${fileId}]` });
|
|
92
93
|
} else if (fileData) {
|
|
93
|
-
// Inline
|
|
94
|
-
|
|
94
|
+
// Inline bytes used to be reduced to a name here, which meant no adapter could forward
|
|
95
|
+
// the attachment even to a target that has a representation for it, and a title-only
|
|
96
|
+
// forward came back as a confident answer about a document the model never saw (#5212).
|
|
97
|
+
// The marker survives on the part for every wire that still cannot carry one. Bytes with
|
|
98
|
+
// no declared media type are not decodable into a document and keep the marker they had.
|
|
99
|
+
const document = inlineDocumentFromDataUrl(fileData, filename);
|
|
100
|
+
parts.push(document ?? { type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" });
|
|
95
101
|
}
|
|
96
102
|
// A bare filename is not a file resource in the Responses schema, so omit it rather than
|
|
97
103
|
// fabricating a "[file: ...]" marker for an attachment that was never sent.
|