@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
package/src/chat/inbound.ts
CHANGED
|
@@ -92,11 +92,32 @@ function userContentToBlocks(content: unknown): Rec[] {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
const videoUrl = videoUrlFromPart(raw);
|
|
95
|
-
if (videoUrl)
|
|
95
|
+
if (videoUrl) {
|
|
96
|
+
blocks.push({ type: "input_video", video_url: videoUrl });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const file = fileFromPart(raw);
|
|
100
|
+
if (file) blocks.push(file);
|
|
96
101
|
}
|
|
97
102
|
return blocks;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A Chat Completions `file` part carrying inline bytes, as the Responses `input_file` block.
|
|
107
|
+
*
|
|
108
|
+
* Nothing here recognized the shape, so the part reached the end of the loop with no branch and
|
|
109
|
+
* was dropped in silence (#5212). A part with no inline bytes is still not translatable and is
|
|
110
|
+
* left to the untranslated-media refusal, which runs before this loop.
|
|
111
|
+
*/
|
|
112
|
+
function fileFromPart(part: Rec): Rec | null {
|
|
113
|
+
if (part.type !== "file" && part.type !== "input_file") return null;
|
|
114
|
+
const file = isRec(part.file) ? part.file : part;
|
|
115
|
+
const fileData = file.file_data;
|
|
116
|
+
if (typeof fileData !== "string" || fileData.length === 0) return null;
|
|
117
|
+
const filename = typeof file.filename === "string" && file.filename.length > 0 ? file.filename : undefined;
|
|
118
|
+
return { type: "input_file", file_data: fileData, ...(filename ? { filename } : {}) };
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
/**
|
|
101
122
|
* The assistant's prior thinking, as plaintext, from either Chat spelling.
|
|
102
123
|
*
|
|
@@ -146,7 +167,27 @@ function pushSystemText(parts: string[], content: unknown): void {
|
|
|
146
167
|
if (text) parts.push(text);
|
|
147
168
|
}
|
|
148
169
|
|
|
149
|
-
|
|
170
|
+
/**
|
|
171
|
+
* A mid-conversation instruction, as the input item the rest of the pipeline already reads.
|
|
172
|
+
*
|
|
173
|
+
* The role is `developer` rather than `system` for two reasons that both bite. The native
|
|
174
|
+
* ChatGPT backend refuses a `role:"system"` item inside `input`, and canonical forwarding
|
|
175
|
+
* folds every message-shaped `system` item back onto `instructions`
|
|
176
|
+
* (src/adapters/openai-responses/canonical-forward.ts), which would undo the placement one hop
|
|
177
|
+
* later. `developer` is first-class in responsesRequestSchema, survives parseRequest as a
|
|
178
|
+
* chronological conversation message, and is exactly what src/claude/inbound.ts already emits
|
|
179
|
+
* for the same shape.
|
|
180
|
+
*/
|
|
181
|
+
function developerInstructionItem(text: string): Rec {
|
|
182
|
+
return { type: "message", role: "developer", content: [{ type: "input_text", text }] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toolCallsToItems(
|
|
186
|
+
toolCalls: unknown,
|
|
187
|
+
input: Rec[],
|
|
188
|
+
knownNameByCallId: Map<string, string>,
|
|
189
|
+
awaitingToolResult: Set<string>,
|
|
190
|
+
): void {
|
|
150
191
|
if (!Array.isArray(toolCalls)) return;
|
|
151
192
|
for (const raw of toolCalls) {
|
|
152
193
|
if (!isRec(raw)) continue;
|
|
@@ -166,6 +207,7 @@ function toolCallsToItems(toolCalls: unknown, input: Rec[], knownNameByCallId: M
|
|
|
166
207
|
if (!name) throw new ChatCompletionsRequestError("tool_calls entries require function.name");
|
|
167
208
|
knownNameByCallId.set(callId, name);
|
|
168
209
|
input.push({ type: "function_call", call_id: callId, name, arguments: args });
|
|
210
|
+
awaitingToolResult.add(callId);
|
|
169
211
|
}
|
|
170
212
|
}
|
|
171
213
|
|
|
@@ -218,11 +260,67 @@ function toolChoiceToResponses(choice: unknown, body: Rec): void {
|
|
|
218
260
|
body.tool_choice = { type: "function", name };
|
|
219
261
|
return;
|
|
220
262
|
}
|
|
263
|
+
if (choice.type === "allowed_tools") {
|
|
264
|
+
body.tool_choice = allowedToolsChoiceToResponses(choice);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
221
267
|
if (isRec(choice.function) && typeof choice.function.name === "string") {
|
|
222
268
|
body.tool_choice = { type: "function", name: choice.function.name };
|
|
223
269
|
}
|
|
224
270
|
}
|
|
225
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Chat Completions nests the subset under `allowed_tools`, Responses carries `mode`/`tools`
|
|
274
|
+
* on the choice itself, and each entry names its tool under a member keyed by its own type
|
|
275
|
+
* (`{"type":"function","function":{"name"}}`) rather than a flat `name`. Neither level lines up
|
|
276
|
+
* with `mapToolChoice`, so an unflattened choice fell past every branch and the caller's subset
|
|
277
|
+
* was dropped while the full catalogue was still advertised (#5211).
|
|
278
|
+
*
|
|
279
|
+
* An entry nobody can name is refused rather than skipped: dropping one widens the very subset
|
|
280
|
+
* the caller sent this field to narrow.
|
|
281
|
+
*/
|
|
282
|
+
function allowedToolsChoiceToResponses(choice: Rec): Rec {
|
|
283
|
+
const spec = isRec(choice.allowed_tools) ? choice.allowed_tools : choice;
|
|
284
|
+
if (!Array.isArray(spec.tools) || spec.tools.length === 0) {
|
|
285
|
+
throw new ChatCompletionsRequestError("tool_choice.allowed_tools requires a non-empty tools array");
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
type: "allowed_tools",
|
|
289
|
+
mode: spec.mode === "required" ? "required" : "auto",
|
|
290
|
+
tools: spec.tools.map(allowedToolEntryToResponses),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Hosted entries are named by their type alone; a function or custom entry must carry a name. */
|
|
295
|
+
const HOSTED_ALLOWED_TOOL_TYPES = new Set([
|
|
296
|
+
"web_search",
|
|
297
|
+
"web_search_preview",
|
|
298
|
+
"image_generation",
|
|
299
|
+
"image_gen",
|
|
300
|
+
"tool_search",
|
|
301
|
+
]);
|
|
302
|
+
const NAMED_ALLOWED_TOOL_TYPES = new Set(["function", "custom"]);
|
|
303
|
+
|
|
304
|
+
function allowedToolEntryToResponses(raw: unknown): Rec {
|
|
305
|
+
if (!isRec(raw)) {
|
|
306
|
+
throw new ChatCompletionsRequestError("tool_choice.allowed_tools.tools entries must be objects");
|
|
307
|
+
}
|
|
308
|
+
const type = typeof raw.type === "string" && raw.type.length > 0 ? raw.type : "function";
|
|
309
|
+
if (!NAMED_ALLOWED_TOOL_TYPES.has(type) && !HOSTED_ALLOWED_TOOL_TYPES.has(type)) {
|
|
310
|
+
// An unknown selector kind is not a narrower subset, it is a subset nobody can evaluate.
|
|
311
|
+
throw new ChatCompletionsRequestError(`unsupported tool_choice.allowed_tools.tools entry type: ${type}`);
|
|
312
|
+
}
|
|
313
|
+
const nested = isRec(raw[type]) ? raw[type] as Rec : undefined;
|
|
314
|
+
const name = typeof raw.name === "string" && raw.name.length > 0
|
|
315
|
+
? raw.name
|
|
316
|
+
: nested !== undefined && typeof nested.name === "string" && nested.name.length > 0
|
|
317
|
+
? nested.name
|
|
318
|
+
: undefined;
|
|
319
|
+
if (name !== undefined) return { type, name };
|
|
320
|
+
if (HOSTED_ALLOWED_TOOL_TYPES.has(type)) return { type };
|
|
321
|
+
throw new ChatCompletionsRequestError("tool_choice.allowed_tools.tools entries require a name");
|
|
322
|
+
}
|
|
323
|
+
|
|
226
324
|
function responseFormatToText(format: unknown): Rec | undefined {
|
|
227
325
|
if (format === undefined) return undefined;
|
|
228
326
|
if (!isRec(format)) throw new ChatCompletionsRequestError("response_format must be an object");
|
|
@@ -291,21 +389,54 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
291
389
|
// Recover replace-style tool calls incrementally instead of rebuilding the
|
|
292
390
|
// call-id index from the entire translated transcript for every message.
|
|
293
391
|
const knownNameByCallId = new Map<string, string>();
|
|
392
|
+
// Tool calls whose result has not arrived yet. Several adapters need a call and its output
|
|
393
|
+
// to stay adjacent — Kiro refuses an interrupted pair (src/adapters/kiro/payload.ts) and the
|
|
394
|
+
// Anthropic and Google mappers synthesize a missing result — so an instruction that arrives
|
|
395
|
+
// inside an open batch waits for the batch to drain instead of splitting it.
|
|
396
|
+
const awaitingToolResult = new Set<string>();
|
|
397
|
+
const heldInstructions: string[] = [];
|
|
398
|
+
const releaseHeldInstructions = (): void => {
|
|
399
|
+
if (heldInstructions.length === 0) return;
|
|
400
|
+
input.push(developerInstructionItem(heldInstructions.join("\n\n")));
|
|
401
|
+
heldInstructions.length = 0;
|
|
402
|
+
};
|
|
403
|
+
// A user or assistant turn ends any open tool batch, so held text rejoins the timeline
|
|
404
|
+
// before that turn rather than drifting past it.
|
|
405
|
+
const beginConversationTurn = (): void => {
|
|
406
|
+
releaseHeldInstructions();
|
|
407
|
+
awaitingToolResult.clear();
|
|
408
|
+
};
|
|
294
409
|
|
|
295
410
|
for (const msg of raw.messages) {
|
|
296
411
|
if (!isRec(msg)) continue;
|
|
297
412
|
const role = typeof msg.role === "string" ? msg.role : "";
|
|
298
413
|
switch (role) {
|
|
299
414
|
case "system":
|
|
300
|
-
case "developer":
|
|
301
|
-
|
|
415
|
+
case "developer": {
|
|
416
|
+
// A leading block is this request's instructions and keeps that treatment: it is the
|
|
417
|
+
// prompt head, and hoisting it is what the upstream prefix cache wants.
|
|
418
|
+
if (input.length === 0) {
|
|
419
|
+
pushSystemText(systemParts, msg.content);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
// Past the first turn the slot carries meaning. `U1 -> A1 -> D2 -> U2` says D2 applies
|
|
423
|
+
// to U2 and not to U1, and folding it into `instructions` moved it ahead of both while
|
|
424
|
+
// rewriting the prompt head on every turn that carried one. The outbound adapter has
|
|
425
|
+
// preserved this slot since #4161; the position was already gone by the time it ran.
|
|
426
|
+
const text = contentToText(msg.content).trim();
|
|
427
|
+
if (!text) break;
|
|
428
|
+
if (awaitingToolResult.size > 0) heldInstructions.push(text);
|
|
429
|
+
else input.push(developerInstructionItem(text));
|
|
302
430
|
break;
|
|
431
|
+
}
|
|
303
432
|
case "user": {
|
|
433
|
+
beginConversationTurn();
|
|
304
434
|
const blocks = userContentToBlocks(msg.content);
|
|
305
435
|
if (blocks.length > 0) input.push({ type: "message", role: "user", content: blocks });
|
|
306
436
|
break;
|
|
307
437
|
}
|
|
308
438
|
case "assistant": {
|
|
439
|
+
beginConversationTurn();
|
|
309
440
|
// A reasoning item precedes the assistant message it belongs to: the
|
|
310
441
|
// Responses assistant item schema admits only output content blocks, so there
|
|
311
442
|
// is no attachment point on the message itself, and the parser buffers a
|
|
@@ -328,7 +459,9 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
328
459
|
}
|
|
329
460
|
const blocks = assistantContentToBlocks(msg.content);
|
|
330
461
|
if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
|
|
331
|
-
if (msg.tool_calls !== undefined)
|
|
462
|
+
if (msg.tool_calls !== undefined) {
|
|
463
|
+
toolCallsToItems(msg.tool_calls, input, knownNameByCallId, awaitingToolResult);
|
|
464
|
+
}
|
|
332
465
|
break;
|
|
333
466
|
}
|
|
334
467
|
case "function": {
|
|
@@ -351,12 +484,15 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
351
484
|
? blocks.filter(part => part.type === "input_text" || part.type === "input_image")
|
|
352
485
|
: contentToText(msg.content);
|
|
353
486
|
input.push({ type: "function_call_output", call_id: callId, output });
|
|
487
|
+
awaitingToolResult.delete(callId);
|
|
488
|
+
if (awaitingToolResult.size === 0) releaseHeldInstructions();
|
|
354
489
|
break;
|
|
355
490
|
}
|
|
356
491
|
default:
|
|
357
492
|
break;
|
|
358
493
|
}
|
|
359
494
|
}
|
|
495
|
+
releaseHeldInstructions();
|
|
360
496
|
|
|
361
497
|
if (input.length === 0 && systemParts.length === 0) {
|
|
362
498
|
throw new ChatCompletionsRequestError("messages must include at least one user/assistant/tool turn");
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -473,6 +473,12 @@ export function removeDesktop3pStandardPivot(
|
|
|
473
473
|
options: Desktop3pConfigLibraryOptions & {
|
|
474
474
|
appliedFingerprint?: string | null; unlink?: (path: string) => void;
|
|
475
475
|
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
476
|
+
/**
|
|
477
|
+
* The desired-state guard below exists for OFF flows racing a concurrent enable. A
|
|
478
|
+
* mode switch (gateway → first-party) removes the profile while the integration stays
|
|
479
|
+
* ON on purpose, so the caller opts out of that guard.
|
|
480
|
+
*/
|
|
481
|
+
replaceWhileEnabled?: boolean;
|
|
476
482
|
} = {},
|
|
477
483
|
): Desktop3pRemovalResult {
|
|
478
484
|
const libraryPath = resolveDesktop3pConfigLibraryPath(options);
|
|
@@ -488,7 +494,7 @@ export function removeDesktop3pStandardPivot(
|
|
|
488
494
|
}
|
|
489
495
|
const latest = readConfigDiagnostics();
|
|
490
496
|
if (latest.source === "fallback") return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: "desktop_config_invalid" };
|
|
491
|
-
if (claudeDesktopIntegrationEnabled(latest.config)) {
|
|
497
|
+
if (!options.replaceWhileEnabled && claudeDesktopIntegrationEnabled(latest.config)) {
|
|
492
498
|
const observed = inspectDesktop3pConfigLibrary(options);
|
|
493
499
|
if (observed.kind === "not_installed" || observed.kind === "no_owned_state") {
|
|
494
500
|
return { ok: true, changed: false, kind: "noop", libraryPath };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Desktop first-party mode.
|
|
3
|
+
*
|
|
4
|
+
* Desktop has two ways to reach opencodex:
|
|
5
|
+
*
|
|
6
|
+
* - `first-party` (default): the app keeps its ordinary claude.ai login, Chat tab, connectors
|
|
7
|
+
* and remote control. Only the Claude Code process it spawns for the Code tab (and that
|
|
8
|
+
* process's subagents) is redirected, through the `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` env
|
|
9
|
+
* in `~/.claude/settings.json` (src/claude/intercept/settings.ts) and the server's intercept
|
|
10
|
+
* pair (src/claude/intercept/runtime.ts). Nothing is written under Desktop's config library.
|
|
11
|
+
* - `gateway`: the historical third-party deployment profile (src/claude/desktop-3p.ts). The
|
|
12
|
+
* whole app is switched to a gateway build; picker entries are opencodex aliases.
|
|
13
|
+
*
|
|
14
|
+
* The two are mutually exclusive on disk: applying one removes the other. The mode is persisted
|
|
15
|
+
* in `claudeCode.desktopMode`; installs that predate the field but already carry an applied
|
|
16
|
+
* gateway profile keep `gateway` until they explicitly re-apply, so an update never flips a
|
|
17
|
+
* working Desktop under the operator.
|
|
18
|
+
*/
|
|
19
|
+
import { getConfigDir } from "../config/paths";
|
|
20
|
+
import type { OcxConfig } from "../types";
|
|
21
|
+
import { claudeInterceptCaCertPath, ensureLocalInterceptCa } from "./intercept/local-ca";
|
|
22
|
+
import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/runtime";
|
|
23
|
+
import {
|
|
24
|
+
applyClaudeInterceptSettings,
|
|
25
|
+
buildClaudeInterceptEnv,
|
|
26
|
+
captureClaudeInterceptSettingsRollback,
|
|
27
|
+
inspectClaudeInterceptSettings,
|
|
28
|
+
removeClaudeInterceptSettings,
|
|
29
|
+
type ClaudeInterceptEnv,
|
|
30
|
+
type ClaudeInterceptSettingsState,
|
|
31
|
+
type ClaudeInterceptSettingsWrite,
|
|
32
|
+
} from "./intercept/settings";
|
|
33
|
+
|
|
34
|
+
export const CLAUDE_DESKTOP_MODES = ["first-party", "gateway"] as const;
|
|
35
|
+
export type ClaudeDesktopMode = typeof CLAUDE_DESKTOP_MODES[number];
|
|
36
|
+
export const DEFAULT_CLAUDE_DESKTOP_MODE: ClaudeDesktopMode = "first-party";
|
|
37
|
+
|
|
38
|
+
export function isClaudeDesktopMode(value: unknown): value is ClaudeDesktopMode {
|
|
39
|
+
return typeof value === "string" && (CLAUDE_DESKTOP_MODES as readonly string[]).includes(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type DesktopModeConfig = Pick<OcxConfig, "claudeCode">;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Effective Desktop mode. An explicit `claudeCode.desktopMode` wins; otherwise a persisted
|
|
46
|
+
* gateway apply marker (`desktopProfile.appliedFingerprint`) means a pre-existing gateway
|
|
47
|
+
* install and keeps `gateway`; everything else is the first-party default.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveClaudeDesktopMode(config: DesktopModeConfig): ClaudeDesktopMode {
|
|
50
|
+
const explicit = config.claudeCode?.desktopMode;
|
|
51
|
+
if (isClaudeDesktopMode(explicit)) return explicit;
|
|
52
|
+
if (config.claudeCode?.desktopProfile?.appliedFingerprint) return "gateway";
|
|
53
|
+
return DEFAULT_CLAUDE_DESKTOP_MODE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Config mutation that records the applied Desktop mode. Switching to first-party also drops
|
|
58
|
+
* the gateway apply marker: the profile assignments stay for a later gateway apply, but a
|
|
59
|
+
* stale `appliedFingerprint` must not make `resolveClaudeDesktopMode` read `gateway` again
|
|
60
|
+
* should the explicit marker ever go missing.
|
|
61
|
+
*/
|
|
62
|
+
export function recordClaudeDesktopMode(
|
|
63
|
+
config: DesktopModeConfig,
|
|
64
|
+
mode: ClaudeDesktopMode,
|
|
65
|
+
): { changed: boolean; value: true } {
|
|
66
|
+
const claudeCode = config.claudeCode ?? {};
|
|
67
|
+
const profile = claudeCode.desktopProfile;
|
|
68
|
+
const dropMarker = mode === "first-party" && profile !== undefined
|
|
69
|
+
&& (profile.appliedFingerprint !== undefined || profile.appliedAt !== undefined);
|
|
70
|
+
if (claudeCode.desktopMode === mode && !dropMarker) return { changed: false, value: true };
|
|
71
|
+
if (dropMarker) {
|
|
72
|
+
const { appliedFingerprint: _fingerprint, appliedAt: _at, ...rest } = profile;
|
|
73
|
+
config.claudeCode = { ...claudeCode, desktopMode: mode, desktopProfile: rest };
|
|
74
|
+
} else {
|
|
75
|
+
config.claudeCode = { ...claudeCode, desktopMode: mode };
|
|
76
|
+
}
|
|
77
|
+
return { changed: true, value: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mode an *apply* without an explicit choice should use. The first-party default only holds
|
|
82
|
+
* where the intercept proxy actually runs; with it disabled (or on a client role) an implied
|
|
83
|
+
* first-party apply would point Claude Code at a proxy that never starts, so fall back to the
|
|
84
|
+
* gateway profile. An explicit `desktopMode: "first-party"` is still honoured (and refused
|
|
85
|
+
* later with `intercept_disabled`, which names the fix).
|
|
86
|
+
*/
|
|
87
|
+
export function resolveClaudeDesktopApplyMode(
|
|
88
|
+
config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
|
|
89
|
+
): ClaudeDesktopMode {
|
|
90
|
+
const resolved = resolveClaudeDesktopMode(config);
|
|
91
|
+
if (resolved === "gateway" || isClaudeDesktopMode(config.claudeCode?.desktopMode)) return resolved;
|
|
92
|
+
return claudeInterceptEnabled(config) ? "first-party" : "gateway";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DesktopFirstPartyTarget {
|
|
96
|
+
proxyPort: number;
|
|
97
|
+
caCertPath: string;
|
|
98
|
+
env: ClaudeInterceptEnv;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The settings env a first-party apply on this machine writes (CA is created on demand). */
|
|
102
|
+
export function desktopFirstPartyTarget(
|
|
103
|
+
config: Pick<OcxConfig, "claudeCode" | "port">,
|
|
104
|
+
opencodexConfigDir = getConfigDir(),
|
|
105
|
+
): DesktopFirstPartyTarget {
|
|
106
|
+
const proxyPort = claudeInterceptProxyPort(config, config.port ?? 10100);
|
|
107
|
+
const caCertPath = claudeInterceptCaCertPath(opencodexConfigDir);
|
|
108
|
+
return { proxyPort, caCertPath, env: buildClaudeInterceptEnv(proxyPort, caCertPath) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface DesktopFirstPartyInspection {
|
|
112
|
+
/** False when the server will not run the intercept pair (client role, intercept disabled). */
|
|
113
|
+
interceptEnabled: boolean;
|
|
114
|
+
proxyPort: number;
|
|
115
|
+
caCertPath: string;
|
|
116
|
+
settings: ClaudeInterceptSettingsState;
|
|
117
|
+
/** settings.json carries exactly the env the current config would write. */
|
|
118
|
+
applied: boolean;
|
|
119
|
+
/** Ours, but for an older port/config directory. Re-apply refreshes it. */
|
|
120
|
+
stale: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface DesktopFirstPartyOptions {
|
|
124
|
+
opencodexConfigDir?: string;
|
|
125
|
+
claudeConfigDir?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Prepare rollback before replacing a gateway, without changing settings. */
|
|
129
|
+
export function captureDesktopFirstPartyRollback(
|
|
130
|
+
config: Pick<OcxConfig, "claudeCode" | "port">,
|
|
131
|
+
options: DesktopFirstPartyOptions = {},
|
|
132
|
+
): () => boolean {
|
|
133
|
+
return captureClaudeInterceptSettingsRollback(
|
|
134
|
+
desktopFirstPartyTarget(config, options.opencodexConfigDir).env, options.claudeConfigDir,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function inspectDesktopFirstParty(
|
|
139
|
+
config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
|
|
140
|
+
options: DesktopFirstPartyOptions = {},
|
|
141
|
+
): DesktopFirstPartyInspection {
|
|
142
|
+
const target = desktopFirstPartyTarget(config, options.opencodexConfigDir);
|
|
143
|
+
const settings = inspectClaudeInterceptSettings(target.env, options.claudeConfigDir);
|
|
144
|
+
return {
|
|
145
|
+
interceptEnabled: claudeInterceptEnabled(config),
|
|
146
|
+
proxyPort: target.proxyPort,
|
|
147
|
+
caCertPath: target.caCertPath,
|
|
148
|
+
settings,
|
|
149
|
+
applied: settings.kind === "applied",
|
|
150
|
+
stale: settings.kind === "stale",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type DesktopFirstPartyApplyResult =
|
|
155
|
+
| { ok: true; changed: boolean; path: string; env: ClaudeInterceptEnv; proxyPort: number }
|
|
156
|
+
| { ok: false; reason: "intercept_disabled" | "ca_unavailable" | "unreadable" | "foreign_env"; path: string };
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Write the first-party env into Claude Code's settings. Creates the local CA first so the
|
|
160
|
+
* path we point `NODE_EXTRA_CA_CERTS` at exists before Claude Code ever reads it.
|
|
161
|
+
*/
|
|
162
|
+
export function applyDesktopFirstParty(
|
|
163
|
+
config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
|
|
164
|
+
options: DesktopFirstPartyOptions = {},
|
|
165
|
+
): DesktopFirstPartyApplyResult {
|
|
166
|
+
const opencodexConfigDir = options.opencodexConfigDir ?? getConfigDir();
|
|
167
|
+
const target = desktopFirstPartyTarget(config, opencodexConfigDir);
|
|
168
|
+
if (!claudeInterceptEnabled(config)) return { ok: false, reason: "intercept_disabled", path: "" };
|
|
169
|
+
try {
|
|
170
|
+
ensureLocalInterceptCa(opencodexConfigDir);
|
|
171
|
+
} catch {
|
|
172
|
+
return { ok: false, reason: "ca_unavailable", path: target.caCertPath };
|
|
173
|
+
}
|
|
174
|
+
const written = applyClaudeInterceptSettings(target.env, options.claudeConfigDir);
|
|
175
|
+
if (!written.ok) return { ok: false, reason: written.reason, path: written.path };
|
|
176
|
+
return { ok: true, changed: written.changed, path: written.path, env: target.env, proxyPort: target.proxyPort };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Remove the first-party env. Only values anchored on our CA path are touched. */
|
|
180
|
+
export function removeDesktopFirstParty(options: DesktopFirstPartyOptions = {}): ClaudeInterceptSettingsWrite {
|
|
181
|
+
const caCertPath = claudeInterceptCaCertPath(options.opencodexConfigDir ?? getConfigDir());
|
|
182
|
+
return removeClaudeInterceptSettings(caCertPath, options.claudeConfigDir);
|
|
183
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mutatePersistedConfig } from "../config";
|
|
2
|
+
import type { OcxConfig } from "../types";
|
|
3
|
+
import { emptyDesktopProfile, type DesktopProfile } from "./desktop-profile";
|
|
4
|
+
|
|
5
|
+
/** Record the bytes already committed by the gateway writer, before cleanup of
|
|
6
|
+
* the previous mode. Mode and fingerprint belong to one config transaction. */
|
|
7
|
+
export function recordCommittedDesktopGateway(
|
|
8
|
+
config: Pick<OcxConfig, "claudeCode">,
|
|
9
|
+
profile: DesktopProfile | undefined,
|
|
10
|
+
fingerprint: string | undefined,
|
|
11
|
+
appliedAt: string,
|
|
12
|
+
): void {
|
|
13
|
+
const { appliedFingerprint: _oldFingerprint, appliedAt: _oldTime, ...base } = profile ?? emptyDesktopProfile();
|
|
14
|
+
config.claudeCode = {
|
|
15
|
+
...config.claudeCode,
|
|
16
|
+
desktopMode: "gateway",
|
|
17
|
+
desktopProfile: {
|
|
18
|
+
...structuredClone(base),
|
|
19
|
+
...(fingerprint ? { appliedFingerprint: fingerprint, appliedAt } : {}),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function persistCommittedDesktopGateway(
|
|
25
|
+
snapshot: OcxConfig,
|
|
26
|
+
profile: DesktopProfile | undefined,
|
|
27
|
+
fingerprint: string | undefined,
|
|
28
|
+
): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" | "unavailable" } {
|
|
29
|
+
const appliedAt = new Date().toISOString();
|
|
30
|
+
try {
|
|
31
|
+
const outcome = mutatePersistedConfig(current => {
|
|
32
|
+
recordCommittedDesktopGateway(current, profile, fingerprint, appliedAt);
|
|
33
|
+
return { changed: true, value: true };
|
|
34
|
+
});
|
|
35
|
+
if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
|
|
36
|
+
recordCommittedDesktopGateway(snapshot, profile, fingerprint, appliedAt);
|
|
37
|
+
return { ok: true };
|
|
38
|
+
} catch {
|
|
39
|
+
return { ok: false, reason: "unavailable" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -35,6 +35,12 @@ export function toolsToResponses(tools: unknown): Rec[] | undefined {
|
|
|
35
35
|
// call, so carry the source intent instead of the destination default. A
|
|
36
36
|
// non-boolean value is not a valid Anthropic opt-in and must not become one.
|
|
37
37
|
strict: typeof raw.strict === "boolean" ? raw.strict : false,
|
|
38
|
+
// Anthropic restricts who may invoke a tool through allowed_callers. Nothing read it,
|
|
39
|
+
// so the restriction never reached the internal tool and every destination rebuilt the
|
|
40
|
+
// declaration without it while the request still succeeded (#5210).
|
|
41
|
+
...(Array.isArray(raw.allowed_callers)
|
|
42
|
+
? { allowed_callers: raw.allowed_callers.filter((c): c is string => typeof c === "string") }
|
|
43
|
+
: {}),
|
|
38
44
|
});
|
|
39
45
|
continue;
|
|
40
46
|
}
|
package/src/claude/inbound.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, f
|
|
|
19
19
|
import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
|
|
20
20
|
import { stabilizeClaudeInstructionsForPromptCache } from "./inbound-cache-stabilize";
|
|
21
21
|
import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
22
|
+
import { inlineDocumentMarker } from "../responses/inline-document";
|
|
22
23
|
import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
|
|
23
24
|
|
|
24
25
|
|
|
@@ -41,6 +42,26 @@ function imageBlockToInputImage(block: Rec): Rec | null {
|
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function documentTitle(block: Rec): string | undefined {
|
|
46
|
+
return typeof block.title === "string" && block.title.length > 0 ? block.title : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An Anthropic base64 document as the Responses `input_file` block that carries its bytes. */
|
|
50
|
+
function documentBlockToInputFile(block: Rec): Rec | null {
|
|
51
|
+
const source = block.source;
|
|
52
|
+
if (!isRec(source) || source.type !== "base64") return null;
|
|
53
|
+
const mediaType = typeof source.media_type === "string" && source.media_type.length > 0
|
|
54
|
+
? source.media_type
|
|
55
|
+
: "application/octet-stream";
|
|
56
|
+
if (typeof source.data !== "string" || source.data.length === 0) return null;
|
|
57
|
+
const title = documentTitle(block);
|
|
58
|
+
return {
|
|
59
|
+
type: "input_file",
|
|
60
|
+
file_data: `data:${mediaType};base64,${source.data}`,
|
|
61
|
+
...(title !== undefined ? { filename: title } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
function toolResultOutput(block: Rec): string | Rec[] {
|
|
45
66
|
const isError = block.is_error === true;
|
|
46
67
|
const content = block.content;
|
|
@@ -55,9 +76,11 @@ function toolResultOutput(block: Rec): string | Rec[] {
|
|
|
55
76
|
const img = imageBlockToInputImage(item);
|
|
56
77
|
if (img) out.push(img);
|
|
57
78
|
} else if (item.type === "document") {
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
79
|
+
// Tool output has no structured document carrier on this route — the Responses tool
|
|
80
|
+
// output vocabulary has no input_file block, and every adapter's tool-result path
|
|
81
|
+
// flattens to text — so this keeps the #939 marker. The user-message branch below is
|
|
82
|
+
// where bytes survive. Recorded as the remaining half of #5212.
|
|
83
|
+
out.push({ type: "input_text", text: inlineDocumentMarker(documentTitle(item)) });
|
|
61
84
|
}
|
|
62
85
|
}
|
|
63
86
|
if (isError) out.unshift({ type: "input_text", text: "[tool error]" });
|
|
@@ -208,9 +231,12 @@ function userMessageToItems(content: unknown, input: Rec[], elide: SkillElisionC
|
|
|
208
231
|
break;
|
|
209
232
|
}
|
|
210
233
|
case "document":
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
234
|
+
// A base64 document now rides the Responses input_file block, so a target with a
|
|
235
|
+
// counterpart receives the bytes instead of a sentence about them (#5212). Every other
|
|
236
|
+
// source is a reference this route cannot dereference, and keeps the marker #939
|
|
237
|
+
// introduced — which is also what a target with no document representation still sees.
|
|
238
|
+
pending.push(documentBlockToInputFile(raw)
|
|
239
|
+
?? { type: "input_text", text: inlineDocumentMarker(documentTitle(raw)) });
|
|
214
240
|
break;
|
|
215
241
|
default:
|
|
216
242
|
break; // thinking/redacted_thinking never appear in user messages; ignore unknowns
|