@bitkyc08/opencodex 2.86.0 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-CHC89EXQ.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +151 -33
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +52 -9
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +7 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +59 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +77 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +46 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +33 -16
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/loopback-family.ts +31 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +14 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +53 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +657 -297
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +116 -27
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +40 -11
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +6 -5
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +74 -6
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/analytics.ts +7 -15
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +56 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +20 -6
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +8 -8
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +117 -24
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +47 -19
- package/src/server/responses/adapter-delivery.ts +72 -12
- package/src/server/responses/adapter-dispatch.ts +46 -3
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +20 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +121 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +8 -4
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +7 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +23 -3
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +77 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +2 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +63 -9
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +35 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +31 -2
- package/src/types/wire.ts +31 -37
- package/src/types.ts +2 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +30 -11
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +236 -73
- package/src/web-search/progress-stream.ts +15 -2
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/grok/sync.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface GrokSyncDeps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function defaultFetchAllModels(config: OcxConfig): Promise<CatalogModel[]> {
|
|
21
|
-
const { fetchAllModels } = await import("../server/management
|
|
21
|
+
const { fetchAllModels } = await import("../server/management/shared");
|
|
22
22
|
return fetchAllModels(config);
|
|
23
23
|
}
|
|
24
24
|
|
package/src/images/loop.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { createAnthropicThinkingRecovery } from "../server/responses/anthropic-thinking-recovery";
|
|
2
|
+
import { bindAnthropicRejectionResponse } from "../adapters/anthropic-rejection-memo";
|
|
1
3
|
/**
|
|
2
4
|
* Media bridge agentic loop — supports both image and video generation sidecars.
|
|
3
5
|
*
|
|
@@ -20,9 +22,10 @@ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
|
20
22
|
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
21
23
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
22
24
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
25
|
+
import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
|
|
23
26
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
24
27
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
25
|
-
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
|
|
28
|
+
import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
26
29
|
import { rateLimitRetryDelayMs } from "../providers/key-failover";
|
|
27
30
|
import { releaseProviderRequestSlot, sendTrackingRequestSlot, type ProviderRequestSlot } from "../providers/request-pacing";
|
|
28
31
|
import {
|
|
@@ -32,13 +35,14 @@ import {
|
|
|
32
35
|
TRANSLATOR_MAX_TURN_BYTES,
|
|
33
36
|
TranslatorBudgetExceededError,
|
|
34
37
|
} from "../lib/translator-budget";
|
|
35
|
-
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "../web-search/progress-stream";
|
|
38
|
+
import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "../web-search/progress-stream";
|
|
36
39
|
import { fulfillImageCall } from "./fulfill";
|
|
37
40
|
import { parseVideoCallArgs, pollVideoWithHeartbeats, buildVideoResult, createVideoBudget } from "./fulfill-video";
|
|
38
41
|
import { submitVideoJob } from "./xai-video-client";
|
|
39
42
|
import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
|
|
40
43
|
import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
|
|
41
44
|
import type { ImageBridgePlan, VideoBridgePlan } from "./types";
|
|
45
|
+
import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
|
|
42
46
|
|
|
43
47
|
const SSE_HEADERS = {
|
|
44
48
|
"Content-Type": "text/event-stream",
|
|
@@ -241,7 +245,11 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
241
245
|
};
|
|
242
246
|
|
|
243
247
|
for (const e of events) {
|
|
244
|
-
if (e.type === "
|
|
248
|
+
if (e.type === "text_delta" && e.anthropicThinkingSignature) {
|
|
249
|
+
flushVisible();
|
|
250
|
+
flushRaw();
|
|
251
|
+
parts.push({ type: "thinking", thinking: e.text, signature: e.anthropicThinkingSignature });
|
|
252
|
+
} else if (e.type === "thinking_delta") {
|
|
245
253
|
flushRaw();
|
|
246
254
|
thinking += e.thinking;
|
|
247
255
|
} else if (e.type === "reasoning_raw_delta") {
|
|
@@ -264,17 +272,18 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
|
|
|
264
272
|
return parts;
|
|
265
273
|
}
|
|
266
274
|
|
|
267
|
-
function jsonError(status: number, message: string): Response {
|
|
268
|
-
return new Response(JSON.stringify({ error: { message, type: "upstream_error", code:
|
|
275
|
+
function jsonError(status: number, message: string, event?: Extract<AdapterEvent, { type: "error" }>): Response {
|
|
276
|
+
return new Response(JSON.stringify({ error: { message, type: event?.errorType ?? "upstream_error", code: event?.code ?? null,
|
|
277
|
+
...(event?.retryable === false ? { retryable: false } : {}) } }), {
|
|
269
278
|
status,
|
|
270
|
-
headers: { "Content-Type": "application/json" },
|
|
279
|
+
headers: { "Content-Type": "application/json", ...(event?.retryable === false ? { "x-should-retry": "false" } : {}) },
|
|
271
280
|
});
|
|
272
281
|
}
|
|
273
282
|
|
|
274
283
|
/** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
|
|
275
284
|
* non-2xx jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
|
|
276
285
|
class LoopError extends Error {
|
|
277
|
-
constructor(readonly status: number, message: string) {
|
|
286
|
+
constructor(readonly status: number, message: string, readonly event?: Extract<AdapterEvent, { type: "error" }>) {
|
|
278
287
|
super(message);
|
|
279
288
|
this.name = "LoopError";
|
|
280
289
|
}
|
|
@@ -285,7 +294,9 @@ class LoopError extends Error {
|
|
|
285
294
|
* metadata, and the optional image/video bridge plans.
|
|
286
295
|
*/
|
|
287
296
|
export interface ImageBridgeDeps {
|
|
297
|
+
thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
|
|
288
298
|
parsed: OcxParsedRequest;
|
|
299
|
+
clientToolCatalogRestriction?: ClientToolCatalogRestriction;
|
|
289
300
|
adapter: ProviderAdapter;
|
|
290
301
|
incomingMeta: IncomingMeta;
|
|
291
302
|
plan?: ImageBridgePlan;
|
|
@@ -314,11 +325,17 @@ export interface ImageBridgeDeps {
|
|
|
314
325
|
/** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
|
|
315
326
|
fetchImpl?: typeof globalThis.fetch;
|
|
316
327
|
/** Bind physical dispatch to this iteration's built request; pacing remains owned by the loop. */
|
|
317
|
-
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter) => typeof globalThis.fetch;
|
|
328
|
+
fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) => typeof globalThis.fetch;
|
|
318
329
|
/** Reserve the routed provider's next request-start slot before each adapter dispatch. */
|
|
319
330
|
waitForRequestSlot?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
|
|
320
331
|
/** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
|
|
321
332
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
333
|
+
/**
|
|
334
|
+
* After an `on429` rotation, offered the usage of the hidden iterations the left credential
|
|
335
|
+
* served. Returning true means it was billed to that credential's attempt, so `onUsage` will not
|
|
336
|
+
* report it again; the client terminal still includes it.
|
|
337
|
+
*/
|
|
338
|
+
onRotatedUsage?: (usage: OcxUsage) => boolean;
|
|
322
339
|
/**
|
|
323
340
|
* Optional 429 failover for the routed (non-xAI) model. Return a rebuilt adapter for the
|
|
324
341
|
* rotated credential, or null when the pool is exhausted. Async hooks support OAuth refresh;
|
|
@@ -353,6 +370,8 @@ export interface ImageBridgeDeps {
|
|
|
353
370
|
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
354
371
|
/** WebSocket Responses path only — leave response id empty for protocol compatibility. */
|
|
355
372
|
forceEmptyResponseId?: boolean;
|
|
373
|
+
/** Responses inbound only: deliver a non-retryable upstream refusal as `response.failed`. */
|
|
374
|
+
terminalRefusalAsFailed?: boolean;
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
/**
|
|
@@ -377,6 +396,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
377
396
|
let paidImageCalls = 0;
|
|
378
397
|
let paidVideoCalls = 0;
|
|
379
398
|
let hiddenUsage: OcxUsage | undefined;
|
|
399
|
+
// Hidden usage already billed through onRotatedUsage: still on the client terminal, not in
|
|
400
|
+
// ledgerUsage, which is what onUsage reports once a rotation has moved some.
|
|
401
|
+
let rotatedUsage: OcxUsage | undefined;
|
|
402
|
+
let ledgerUsage: OcxUsage | undefined;
|
|
380
403
|
let activeProducers = 0;
|
|
381
404
|
|
|
382
405
|
const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
|
|
@@ -445,6 +468,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
445
468
|
interface IterationResponse {
|
|
446
469
|
response: Response;
|
|
447
470
|
responseAdapter: ProviderAdapter;
|
|
471
|
+
parsed?: OcxParsedRequest;
|
|
472
|
+
retryThinking?: () => Promise<IterationResponse>;
|
|
448
473
|
/** runTurn events are already bounded; consume them without replaying or charging twice. */
|
|
449
474
|
collectedEvents?: AdapterEvent[];
|
|
450
475
|
}
|
|
@@ -569,6 +594,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
569
594
|
// runTurn adapters signal errors via {type:"error"} events, not HTTP status codes.
|
|
570
595
|
const errorEvent = events.find(e => e.type === "error");
|
|
571
596
|
if (errorEvent && errorEvent.type === "error") {
|
|
597
|
+
hiddenUsage = addUsage(hiddenUsage, errorEvent.usage);
|
|
572
598
|
if (errorEvent.code === "translation_buffer_limit") {
|
|
573
599
|
throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
|
|
574
600
|
}
|
|
@@ -614,7 +640,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
614
640
|
cachedRequest = request;
|
|
615
641
|
cachedAdapter = requestAdapter;
|
|
616
642
|
}
|
|
617
|
-
const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? fetchImpl;
|
|
643
|
+
const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter, recovery) ?? fetchImpl;
|
|
618
644
|
let response: Response;
|
|
619
645
|
try {
|
|
620
646
|
if (requestAdapter.fetchResponse) {
|
|
@@ -665,7 +691,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
665
691
|
} finally {
|
|
666
692
|
request.releaseBodyObservation?.();
|
|
667
693
|
}
|
|
668
|
-
|
|
694
|
+
bindAnthropicRejectionResponse(request, response);
|
|
695
|
+
return { response, responseAdapter: requestAdapter, parsed: iterParsed };
|
|
669
696
|
};
|
|
670
697
|
|
|
671
698
|
let prepared = await fetchOnce(adapter);
|
|
@@ -709,15 +736,28 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
709
736
|
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
|
|
710
737
|
iterParsed, prepared.response);
|
|
711
738
|
if (!rotated) break;
|
|
739
|
+
if (hiddenUsage && deps.onRotatedUsage?.(hiddenUsage)) {
|
|
740
|
+
rotatedUsage = addUsage(rotatedUsage, hiddenUsage);
|
|
741
|
+
hiddenUsage = undefined;
|
|
742
|
+
}
|
|
712
743
|
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
713
744
|
adapter = rotated.adapter;
|
|
714
745
|
yield { type: "heartbeat" };
|
|
715
746
|
prepared = await fetchOnce(adapter, rotated.recoveryKind);
|
|
716
747
|
}
|
|
717
748
|
|
|
749
|
+
if (await deps.thinkingRecovery?.prepareHttp(prepared.response, prepared.responseAdapter.name, iterParsed)) {
|
|
750
|
+
try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
751
|
+
cachedRequest = undefined;
|
|
752
|
+
prepared = await fetchOnce(adapter, "opaque-blob-rejection");
|
|
753
|
+
}
|
|
718
754
|
// Final headers have arrived. Clear only the deadline timer before ANY body read.
|
|
719
755
|
headerDeadline.clear();
|
|
720
756
|
if (!prepared.response.ok) {
|
|
757
|
+
if (deps.thinkingRecovery && prepared.responseAdapter.name === "anthropic") {
|
|
758
|
+
const event = await deps.thinkingRecovery.projectHttpFailure(prepared.response, signal);
|
|
759
|
+
throw new LoopError(event.status ?? prepared.response.status, event.message, event);
|
|
760
|
+
}
|
|
721
761
|
let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
|
|
722
762
|
try {
|
|
723
763
|
body = await readBoundedResponseBody(prepared.response, { signal });
|
|
@@ -738,9 +778,21 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
738
778
|
const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
|
|
739
779
|
throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
|
|
740
780
|
}
|
|
741
|
-
return prepared
|
|
781
|
+
return { ...prepared, retryThinking: async () => {
|
|
782
|
+
cachedRequest = undefined;
|
|
783
|
+
headerDeadline = clearableDeadline(connectTimeoutMs, signal);
|
|
784
|
+
try { return await fetchOnce(adapter, "opaque-blob-rejection"); }
|
|
785
|
+
catch (error) {
|
|
786
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
|
|
787
|
+
if (headerDeadline.didExpire()) throw new LoopError(504,
|
|
788
|
+
`Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
|
|
789
|
+
throw error;
|
|
790
|
+
} finally { headerDeadline.clear(); deps.onIterationEnd?.(); }
|
|
791
|
+
} };
|
|
742
792
|
} catch (error) {
|
|
743
793
|
if (isTranslatorBudgetExceededError(error)) throw error;
|
|
794
|
+
if (error instanceof SendBudgetExhaustedError) throw new LoopError(429, error.message,
|
|
795
|
+
{ type: "error", status: 429, errorType: "rate_limit_error", code: error.code, message: error.message, retryable: false });
|
|
744
796
|
if (headerDeadline.didExpire()) {
|
|
745
797
|
throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
|
|
746
798
|
}
|
|
@@ -767,12 +819,31 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
767
819
|
const iterationBudget = prepared.collectedEvents ? undefined : createIterationEventBudget();
|
|
768
820
|
try {
|
|
769
821
|
if (iterationBudget) {
|
|
770
|
-
const parse =
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
822
|
+
const parse = async function* (current: IterationResponse): AsyncGenerator<AdapterEvent> {
|
|
823
|
+
if (!current.response.ok && deps.thinkingRecovery) {
|
|
824
|
+
yield await deps.thinkingRecovery.projectHttpFailure(current.response, signal);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
yield* parseStreamWithProgress(current.response, current.responseAdapter.parseStream.bind(current.responseAdapter), {
|
|
829
|
+
signal, inactivityTimeoutMs: stallTimeoutMs, translatorBudget,
|
|
830
|
+
preserveAdapterError: deps.thinkingRecovery !== undefined,
|
|
831
|
+
onErrorUsage: usage => { hiddenUsage = addUsage(hiddenUsage, usage); },
|
|
832
|
+
});
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (!(error instanceof WebSearchAdapterEventError)) throw error;
|
|
835
|
+
if (!error.replaySafe) yield { type: "heartbeat", replayUnsafe: true };
|
|
836
|
+
yield isNonReplayableResponse(current.response)
|
|
837
|
+
? { ...error.event, message: "Provider stream error: upstream diagnostic withheld for a non-replayable failure", retryable: false }
|
|
838
|
+
: error.event;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
const source = deps.thinkingRecovery && prepared.retryThinking
|
|
842
|
+
? deps.thinkingRecovery.wrap(parse(prepared), async function* () {
|
|
843
|
+
yield* parse(await prepared.retryThinking!());
|
|
844
|
+
}, () => prepared.responseAdapter.name, !isNonReplayableResponse(prepared.response), prepared.response, prepared.parsed)
|
|
845
|
+
: parse(prepared);
|
|
846
|
+
for await (const event of source) {
|
|
776
847
|
if (event.type === "heartbeat") yield event;
|
|
777
848
|
else {
|
|
778
849
|
iterationBudget.retain(event);
|
|
@@ -786,6 +857,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
786
857
|
throw error;
|
|
787
858
|
}
|
|
788
859
|
if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
|
|
860
|
+
if (error instanceof LoopError) throw error;
|
|
861
|
+
if (error instanceof SendBudgetExhaustedError) throw new LoopError(429, error.message,
|
|
862
|
+
{ type: "error", status: 429, errorType: "rate_limit_error", code: error.code, message: error.message, retryable: false });
|
|
789
863
|
if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
|
|
790
864
|
if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
|
|
791
865
|
throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -803,8 +877,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
803
877
|
if (terminal.code === "translation_buffer_limit") {
|
|
804
878
|
throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
|
|
805
879
|
}
|
|
806
|
-
throw new LoopError(502, terminal.message);
|
|
880
|
+
throw new LoopError(terminal.status ?? 502, terminal.message, terminal);
|
|
807
881
|
}
|
|
882
|
+
deps.thinkingRecovery?.observeIteration(events);
|
|
808
883
|
return scanEventsForImageCall(events, mediaToolNames);
|
|
809
884
|
};
|
|
810
885
|
|
|
@@ -819,7 +894,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
819
894
|
firstPrepared = await prepareIterationDrained(maxRounds <= 0);
|
|
820
895
|
} catch (e) {
|
|
821
896
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
822
|
-
if (e instanceof LoopError) return jsonError(e.status, e.message);
|
|
897
|
+
if (e instanceof LoopError) return jsonError(e.status, e.message, e.event);
|
|
898
|
+
if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
|
|
899
|
+
{ type: "error", status: 429, errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false });
|
|
823
900
|
throw e;
|
|
824
901
|
}
|
|
825
902
|
}
|
|
@@ -863,13 +940,17 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
863
940
|
// Loop (fulfill + re-ask) ONLY when the model's actionable output is purely image_gen. A
|
|
864
941
|
// real tool call means this turn is terminal for Codex — finalize so those calls reach
|
|
865
942
|
// Codex. forceFinal also finalizes.
|
|
866
|
-
|
|
943
|
+
// A refused iteration also finalizes: fulfilling its media call would re-ask the model
|
|
944
|
+
// with the refused context (#7148).
|
|
945
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceFinal
|
|
946
|
+
&& !split.passthrough.some(isTerminalRefusalIncomplete);
|
|
867
947
|
if (!shouldLoop) {
|
|
868
|
-
if (hiddenUsage) {
|
|
948
|
+
if (hiddenUsage || rotatedUsage) {
|
|
869
949
|
for (let i = split.passthrough.length - 1; i >= 0; i--) {
|
|
870
950
|
const e = split.passthrough[i];
|
|
871
951
|
if (e?.type === "done" || e?.type === "incomplete") {
|
|
872
|
-
|
|
952
|
+
ledgerUsage = addUsage(hiddenUsage, e.usage);
|
|
953
|
+
split.passthrough[i] = { ...e, usage: addUsage(rotatedUsage, ledgerUsage) };
|
|
873
954
|
break;
|
|
874
955
|
}
|
|
875
956
|
}
|
|
@@ -881,6 +962,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
881
962
|
// Discarded iteration still contributed tokens — accumulate for the final onUsage.
|
|
882
963
|
takeUsageFrom(split.passthrough);
|
|
883
964
|
|
|
965
|
+
for (const event of split.passthrough) {
|
|
966
|
+
if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
|
|
967
|
+
}
|
|
968
|
+
|
|
884
969
|
// Fulfill each image/video call, then inject ONE assistant turn (thinking once + all tool
|
|
885
970
|
// calls) so Anthropic extended-thinking continuations stay valid across parallel calls.
|
|
886
971
|
const iterationThinking = extractIterationThinking(split.passthrough);
|
|
@@ -1052,6 +1137,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1052
1137
|
});
|
|
1053
1138
|
}
|
|
1054
1139
|
} catch (e) {
|
|
1140
|
+
ledgerUsage = hiddenUsage;
|
|
1141
|
+
const usage = addUsage(rotatedUsage, hiddenUsage);
|
|
1055
1142
|
if (isTranslatorBudgetExceededError(e)) {
|
|
1056
1143
|
yield {
|
|
1057
1144
|
type: "error",
|
|
@@ -1059,14 +1146,14 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1059
1146
|
errorType: "upstream_error",
|
|
1060
1147
|
code: e.code,
|
|
1061
1148
|
message: "upstream translation buffer exceeded the safe limit",
|
|
1062
|
-
...(
|
|
1149
|
+
...(usage ? { usage } : {}),
|
|
1063
1150
|
};
|
|
1064
1151
|
} else {
|
|
1065
1152
|
yield {
|
|
1066
1153
|
type: "error",
|
|
1067
1154
|
message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
|
|
1068
|
-
...(e instanceof LoopError ? { status: e.status } : {}),
|
|
1069
|
-
...(
|
|
1155
|
+
...(e instanceof LoopError ? { status: e.status, ...(e.event ?? {}) } : {}),
|
|
1156
|
+
...(usage ? { usage } : {}),
|
|
1070
1157
|
};
|
|
1071
1158
|
}
|
|
1072
1159
|
return;
|
|
@@ -1079,13 +1166,15 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1079
1166
|
}
|
|
1080
1167
|
|
|
1081
1168
|
const sse = bridgeToResponsesSSE(
|
|
1082
|
-
produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
1169
|
+
guardEmptyToolCatalogStream(produce(), deps.clientToolCatalogRestriction, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
1083
1170
|
internalAbort.abort("client closed responses stream");
|
|
1084
1171
|
}, 2_000,
|
|
1085
1172
|
{
|
|
1086
1173
|
translatorBudget,
|
|
1087
1174
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
1175
|
+
...(deps.clientToolCatalogRestriction ? { ...deps.clientToolCatalogRestriction, enforceDeclaredToolNames: false } : {}),
|
|
1088
1176
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
1177
|
+
...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
|
|
1089
1178
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1090
1179
|
hideRawReasoning: parsed.options.hideRawReasoning,
|
|
1091
1180
|
stallTimeoutSec: deps.stallTimeoutSec,
|
|
@@ -1093,7 +1182,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
1093
1182
|
...(deps.onUsage ? {
|
|
1094
1183
|
// Terminal done/incomplete already includes hiddenUsage (merged above). Do not
|
|
1095
1184
|
// add it again here or request logs double-count multi-iteration image turns.
|
|
1096
|
-
onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
|
|
1185
|
+
onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(rotatedUsage ? ledgerUsage : usage),
|
|
1097
1186
|
} : {}),
|
|
1098
1187
|
...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
|
|
1099
1188
|
},
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
13
13
|
export async function refreshOwnedCatalogIntegrations(
|
|
14
14
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
15
|
-
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder"],
|
|
15
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder", "gentle-shell"],
|
|
16
16
|
options: { refreshOnly?: boolean; admit?: () => boolean } = {},
|
|
17
17
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
18
18
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
@@ -134,6 +134,7 @@ const CLIENT_MANAGED_PATHS = {
|
|
|
134
134
|
qoder: [["providers", OPENCODE_PROVIDER_ID], ["modelConfigs", "customModels", DYNAMIC_SEGMENT]],
|
|
135
135
|
opencode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
|
|
136
136
|
pi: [["providers", OPENCODE_PROVIDER_ID]],
|
|
137
|
+
"gentle-shell": [["providers", OPENCODE_PROVIDER_ID]],
|
|
137
138
|
omp: [["providers", OPENCODE_PROVIDER_ID]],
|
|
138
139
|
hermes: [["providers", OPENCODE_PROVIDER_ID]],
|
|
139
140
|
openclaw: [["models", "providers", OPENCODE_PROVIDER_ID]],
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Design of record: devlog/_fin/260802_client_toggle_api/021 §1.
|
|
10
10
|
*/
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { gentleShellPaths } from "../clients/gentle-shell-path";
|
|
12
13
|
import { assertDroidSettingsUnambiguous } from "./droid-settings";
|
|
13
14
|
import { readPath } from "./merge";
|
|
14
15
|
import type { OwnershipRecord } from "./ownership";
|
|
@@ -505,6 +506,13 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
505
506
|
return { configPath: droidConfigPath(env, home), detectDir };
|
|
506
507
|
},
|
|
507
508
|
},
|
|
509
|
+
"gentle-shell": {
|
|
510
|
+
id: "gentle-shell",
|
|
511
|
+
configPath: (env, home, io) => gentleShellPaths(env, home, io).configPath,
|
|
512
|
+
detectDir: (env, home, io) => gentleShellPaths(env, home, io).detectDir,
|
|
513
|
+
resolvePaths: (env, home, _context, io) => gentleShellPaths(env, home, io),
|
|
514
|
+
unresolvedPathHint: (_env, home = homedir()) => join(home, ".gentle-shell", "agent", "models.json"),
|
|
515
|
+
},
|
|
508
516
|
};
|
|
509
517
|
|
|
510
518
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -36,6 +36,10 @@ export interface CodexAppServerStateResponse {
|
|
|
36
36
|
* The desktop half of a restart. Scalar-only for the same reason as the rest of this
|
|
37
37
|
* contract: pid lists and a closed-vocabulary reason, never a command line or an OS
|
|
38
38
|
* error message, both of which routinely embed a path or a username.
|
|
39
|
+
*
|
|
40
|
+
* `reason: "picker_current"` means the shell was deliberately left running because it
|
|
41
|
+
* launched after the current catalog was written, so only the app-servers restarted and
|
|
42
|
+
* the operator's unsaved client state was kept (#4761).
|
|
39
43
|
*/
|
|
40
44
|
export interface CodexDesktopRestartSummary {
|
|
41
45
|
attempted: boolean;
|
|
@@ -43,8 +47,17 @@ export interface CodexDesktopRestartSummary {
|
|
|
43
47
|
surviving: number[];
|
|
44
48
|
relaunch: "started" | "skipped";
|
|
45
49
|
reason?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Why the desktop was or was not restarted: `stale` (the shell predates the bound
|
|
52
|
+
* catalog, or this request rebound it) and `unknown` (the evidence was unreadable) both
|
|
53
|
+
* take the full restart. Absent on a proxy older than #4761.
|
|
54
|
+
*/
|
|
55
|
+
pickerState?: CodexDesktopPickerState;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
export type CodexDesktopPickerState = "current" | "stale" | "unknown" | "not_running";
|
|
59
|
+
const DESKTOP_PICKER_STATES: readonly string[] = ["current", "stale", "unknown", "not_running"];
|
|
60
|
+
|
|
48
61
|
/** POST response. All four arrays are pid lists — never command lines. */
|
|
49
62
|
export interface CodexRestartResponse {
|
|
50
63
|
success: boolean;
|
|
@@ -130,6 +143,8 @@ export function isCodexRestartResponse(value: unknown): value is CodexRestartRes
|
|
|
130
143
|
if (!isPidList(d.stopped) || !isPidList(d.surviving)) return false;
|
|
131
144
|
if (d.relaunch !== "started" && d.relaunch !== "skipped") return false;
|
|
132
145
|
if (d.reason !== undefined && typeof d.reason !== "string") return false;
|
|
146
|
+
if (d.pickerState !== undefined
|
|
147
|
+
&& (typeof d.pickerState !== "string" || !DESKTOP_PICKER_STATES.includes(d.pickerState))) return false;
|
|
133
148
|
// A started relaunch cannot have left anything behind: the ladder refuses to
|
|
134
149
|
// relaunch beside a survivor precisely so a second shell never appears.
|
|
135
150
|
if (d.relaunch === "started" && (d.surviving as number[]).length > 0) return false;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { connect as tcpConnect,
|
|
2
|
-
import {
|
|
1
|
+
import { connect as tcpConnect, type Socket } from "node:net";
|
|
2
|
+
import { connect as tlsConnect, type TLSSocket } from "node:tls";
|
|
3
3
|
import { desktopProxyFor } from "./desktop-proxy-route";
|
|
4
|
-
import {
|
|
4
|
+
import { dialTunnel, type OpaqueProxyRoute } from "./opaque-proxy-tunnel";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// The shared opaque dialer owns proxy framing; this caller owns fixed-target
|
|
7
|
+
// TLS and bounds the entire connection, including the target TLS handshake.
|
|
8
8
|
export interface DesktopTunnelOptions {
|
|
9
9
|
proxy?: string | false;
|
|
10
10
|
signal?: AbortSignal;
|
|
@@ -15,37 +15,6 @@ export interface DesktopTunnelOptions {
|
|
|
15
15
|
proxyCa?: string;
|
|
16
16
|
}
|
|
17
17
|
const fail = () => new Error("desktop_upstream_connection_failed");
|
|
18
|
-
class HandshakeReader {
|
|
19
|
-
private buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
20
|
-
private failure: Error | null = null;
|
|
21
|
-
private waiting: { ready: () => boolean; resolve: () => void; reject: (error: Error) => void } | null = null;
|
|
22
|
-
private onData = (chunk: Buffer) => {
|
|
23
|
-
if (this.buffer.length + chunk.length > 65536) { this.onFailure(); return; }
|
|
24
|
-
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
25
|
-
if (this.waiting?.ready()) { const current = this.waiting; this.waiting = null; current.resolve(); }
|
|
26
|
-
};
|
|
27
|
-
private onFailure = () => { this.failure = fail(); const current = this.waiting; this.waiting = null; current?.reject(this.failure); };
|
|
28
|
-
constructor(private socket: Socket, private signal: AbortSignal) {
|
|
29
|
-
socket.on("data", this.onData); socket.once("error", this.onFailure); socket.once("end", this.onFailure); socket.once("close", this.onFailure);
|
|
30
|
-
signal.addEventListener("abort", this.onFailure, { once: true }); if (signal.aborted) this.onFailure(); socket.resume();
|
|
31
|
-
}
|
|
32
|
-
write(bytes: Uint8Array | string): void { if (this.failure) throw this.failure; this.socket.write(bytes); }
|
|
33
|
-
private async until(ready: () => boolean): Promise<void> {
|
|
34
|
-
if (this.failure) throw this.failure;
|
|
35
|
-
if (ready()) return;
|
|
36
|
-
if (this.waiting) throw fail();
|
|
37
|
-
await new Promise<void>((resolve, reject) => { this.waiting = { ready, resolve, reject }; });
|
|
38
|
-
}
|
|
39
|
-
private take(length: number): Buffer { const out = this.buffer.subarray(0, length); this.buffer = this.buffer.subarray(length); return out; }
|
|
40
|
-
async readExact(length: number): Promise<Buffer> { await this.until(() => this.buffer.length >= length); return this.take(length); }
|
|
41
|
-
async readHead(): Promise<string> { await this.until(() => this.buffer.indexOf("\r\n\r\n") >= 0); return this.take(this.buffer.indexOf("\r\n\r\n") + 4).toString("latin1"); }
|
|
42
|
-
dispose(): void {
|
|
43
|
-
this.socket.pause(); this.socket.removeListener("data", this.onData); this.socket.removeListener("error", this.onFailure);
|
|
44
|
-
this.socket.removeListener("end", this.onFailure); this.socket.removeListener("close", this.onFailure);
|
|
45
|
-
this.signal.removeEventListener("abort", this.onFailure);
|
|
46
|
-
if (this.buffer.length) this.socket.unshift(this.buffer); this.buffer = Buffer.alloc(0); this.onFailure();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
18
|
function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
|
|
50
19
|
return new Promise((resolve, reject) => {
|
|
51
20
|
const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
|
|
@@ -76,25 +45,21 @@ export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): P
|
|
|
76
45
|
const url = new URL(proxy), protocol = url.protocol;
|
|
77
46
|
if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
|
|
78
47
|
const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const authority = `${target.host}:${target.port}`;
|
|
90
|
-
const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`];
|
|
91
|
-
if (url.username || url.password) lines.push(`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`);
|
|
92
|
-
reader.write(lines.join("\r\n") + "\r\n\r\n");
|
|
93
|
-
if (!/^HTTP\/1\.[01] 2\d\d(?: |\r)/.test(await reader.readHead())) throw fail();
|
|
94
|
-
}
|
|
95
|
-
} finally { reader.dispose(); }
|
|
48
|
+
const credentials = url.username || url.password
|
|
49
|
+
? { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password) }
|
|
50
|
+
: {};
|
|
51
|
+
const endpoint = { host, port, ...credentials };
|
|
52
|
+
const route: OpaqueProxyRoute = protocol === "https:"
|
|
53
|
+
? { ...endpoint, kind: "https", proxyCa: options.proxyCa }
|
|
54
|
+
: { ...endpoint, kind: protocol.slice(0, -1) as "http" | "socks5" | "socks5h" };
|
|
55
|
+
const authority = `${target.host.includes(":") ? `[${target.host}]` : target.host}:${target.port}`;
|
|
56
|
+
// The route resolver remains authoritative; a failed proxy never becomes DIRECT.
|
|
57
|
+
raw = own(await dialTunnel(authority, route, { signal, timeoutMs: timeout, maxHeaderBytes: 65536 }));
|
|
96
58
|
}
|
|
97
59
|
if (signal.aborted || raw.destroyed) throw fail();
|
|
60
|
+
// This consumer historically closes its writable side after peer EOF. The
|
|
61
|
+
// shared opaque transport preserves half-closes for callers that need them.
|
|
62
|
+
raw.allowHalfOpen = false;
|
|
98
63
|
const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
|
|
99
64
|
// The CONNECT reader paused its stream before handing buffered bytes back. Bun's
|
|
100
65
|
// TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeDeclaredToolName, type AdapterEvent } from "../types";
|
|
2
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "./errors";
|
|
3
|
+
import { releaseTranslatedEvent, type TranslatorBudget } from "./translator-budget";
|
|
4
|
+
import { releaseCompactionCiphertextLease } from "../responses/compaction";
|
|
5
|
+
|
|
6
|
+
/** Snapshot of current-turn declarations when an explicit empty catalog activates enforcement. */
|
|
7
|
+
export interface ClientToolCatalogRestriction {
|
|
8
|
+
declaredToolNames: ReadonlySet<string>;
|
|
9
|
+
bareCustomToolNames: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emptyToolCatalogRefusal(
|
|
13
|
+
event: AdapterEvent,
|
|
14
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
15
|
+
): Extract<AdapterEvent, { type: "error" }> | undefined {
|
|
16
|
+
if (!catalog) return;
|
|
17
|
+
if (event.type === "tool_call_start") {
|
|
18
|
+
const name = normalizeDeclaredToolName(event.name, catalog.declaredToolNames, undefined, catalog.bareCustomToolNames);
|
|
19
|
+
if (catalog.declaredToolNames.has(name)) return;
|
|
20
|
+
} else if (catalog.declaredToolNames.size !== 0
|
|
21
|
+
|| (event.type !== "tool_call_delta" && event.type !== "tool_call_end")) return;
|
|
22
|
+
return {
|
|
23
|
+
type: "error", status: 502, errorType: "upstream_error", code: UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
24
|
+
message: "routed provider emitted a client tool call outside the current request's explicit tool catalog",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Runs after hosted tool loops, before client delivery and continuation-state folding. */
|
|
29
|
+
export async function* guardEmptyToolCatalogStream(
|
|
30
|
+
events: AsyncIterable<AdapterEvent>,
|
|
31
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
32
|
+
budget: TranslatorBudget,
|
|
33
|
+
): AsyncGenerator<AdapterEvent> {
|
|
34
|
+
for await (const event of events) {
|
|
35
|
+
const refusal = emptyToolCatalogRefusal(event, catalog);
|
|
36
|
+
if (refusal) {
|
|
37
|
+
releaseTranslatedEvent(event, budget);
|
|
38
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
39
|
+
}
|
|
40
|
+
yield refusal ?? event;
|
|
41
|
+
if (refusal) return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function guardEmptyToolCatalogEvents(
|
|
46
|
+
events: AdapterEvent[],
|
|
47
|
+
catalog: ClientToolCatalogRestriction | undefined,
|
|
48
|
+
budget: TranslatorBudget,
|
|
49
|
+
): AdapterEvent[] {
|
|
50
|
+
if (!catalog) return events;
|
|
51
|
+
const index = events.findIndex(event => emptyToolCatalogRefusal(event, catalog) !== undefined);
|
|
52
|
+
if (index < 0) return events;
|
|
53
|
+
const refusal = emptyToolCatalogRefusal(events[index]!, catalog)!;
|
|
54
|
+
for (const event of events.slice(index)) {
|
|
55
|
+
if ((event.type === "done" || event.type === "incomplete" || event.type === "error") && event.usage) {
|
|
56
|
+
refusal.usage = event.usage;
|
|
57
|
+
}
|
|
58
|
+
releaseTranslatedEvent(event, budget);
|
|
59
|
+
releaseCompactionCiphertextLease(event, budget);
|
|
60
|
+
}
|
|
61
|
+
return [...events.slice(0, index), refusal];
|
|
62
|
+
}
|