@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/codex/sync.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { approvedCodexProviders } from "./provider-routing";
|
|
1
2
|
import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
|
|
2
3
|
import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE, unbackedRoutedRemovalMessage } from "./catalog/routed-removal";
|
|
3
4
|
import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
|
|
@@ -33,6 +34,8 @@ export interface CodexSyncResult {
|
|
|
33
34
|
catalogPath: string | null;
|
|
34
35
|
catalogExists: boolean;
|
|
35
36
|
catalogWritten: boolean;
|
|
37
|
+
/** Committed configuration-byte change, never inferred from a successful no-op. */
|
|
38
|
+
configChanged?: boolean;
|
|
36
39
|
cacheSynced: boolean;
|
|
37
40
|
/**
|
|
38
41
|
* Whether the catalog owner committed a validated catalog or refused the
|
|
@@ -188,7 +191,7 @@ export async function syncModelsToCodex(
|
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
if (externalProvider) {
|
|
194
|
+
if (externalProvider && !approvedCodexProviders().includes(externalProvider)) {
|
|
192
195
|
if (catalogEvenWhenNotInjected) {
|
|
193
196
|
// External providers own config.toml, and the injector removes the OpenCodex
|
|
194
197
|
// journal for external providers (inject.ts). This explicit catalog-only sync
|
|
@@ -214,6 +217,7 @@ export async function syncModelsToCodex(
|
|
|
214
217
|
return {
|
|
215
218
|
status: "applied",
|
|
216
219
|
ok: result.success,
|
|
220
|
+
...(result.success && result.configChanged === true ? { configChanged: true } : {}),
|
|
217
221
|
added: 0,
|
|
218
222
|
catalogPath: null,
|
|
219
223
|
catalogExists: false,
|
|
@@ -332,6 +336,7 @@ export async function syncModelsToCodex(
|
|
|
332
336
|
return {
|
|
333
337
|
status: "applied",
|
|
334
338
|
ok: result.success,
|
|
339
|
+
...(result.success && result.configChanged === true ? { configChanged: true } : {}),
|
|
335
340
|
added,
|
|
336
341
|
...(refreshOutcome ? { refreshOutcome } : {}),
|
|
337
342
|
catalogPath,
|
|
@@ -19,6 +19,7 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
19
19
|
|
|
20
20
|
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
|
|
21
21
|
import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
|
|
22
|
+
import { resolveWindowsLocalAppData } from "./windows-local-appdata";
|
|
22
23
|
|
|
23
24
|
import type {
|
|
24
25
|
ResolveCodexCoordinatorDatabasePath,
|
|
@@ -209,10 +210,11 @@ export function decodeWindowsIdentityPowerShellOutputForTests(output: Uint8Array
|
|
|
209
210
|
/**
|
|
210
211
|
* Per-process memo for the Windows lookups.
|
|
211
212
|
*
|
|
212
|
-
* Both values -- the effective token's SID and its known-folder local AppData --
|
|
213
|
+
* Both helper values -- the effective token's SID and its known-folder local AppData --
|
|
213
214
|
* are fixed for the lifetime of a process: neither can change without a new logon
|
|
214
215
|
* token, and the lookups deliberately ignore the environment, so nothing a caller
|
|
215
|
-
* does between two calls can alter the answer.
|
|
216
|
+
* does between two calls can alter the answer. The native x64 known-folder path
|
|
217
|
+
* avoids this helper entirely. Each fallback call otherwise spawns a fresh
|
|
216
218
|
* PowerShell, roughly 150ms for the SID and 310ms for the folder, and the
|
|
217
219
|
* coordinator asks for both on every config write and lock acquisition. That cost
|
|
218
220
|
* pushed real multi-process injection tests past their budget while proving
|
|
@@ -265,6 +267,15 @@ function resolveWindowsSid(): string {
|
|
|
265
267
|
return sid.toUpperCase();
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
function windowsLocalAppData(): string {
|
|
271
|
+
try {
|
|
272
|
+
return resolveWindowsLocalAppData(() => powershellValue(windowsLocalAppDataExpression()));
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
if (cause instanceof CodexUserIdentityRefusal) throw cause;
|
|
275
|
+
refuse("Windows LocalAppData resolution failed.", cause);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
268
279
|
export const resolveEffectiveUserIdentity: ResolveEffectiveUserIdentity = () => {
|
|
269
280
|
if (process.platform === "win32") {
|
|
270
281
|
return { platform: "win32", sid: resolveWindowsSid() };
|
|
@@ -374,7 +385,7 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
|
|
|
374
385
|
}
|
|
375
386
|
|
|
376
387
|
if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
|
|
377
|
-
const localAppData =
|
|
388
|
+
const localAppData = windowsLocalAppData();
|
|
378
389
|
if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
|
|
379
390
|
const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
|
|
380
391
|
let entry;
|
|
@@ -404,10 +415,10 @@ export function probeCodexCoordinatorNamespace(identity: UserIdentity): Coordina
|
|
|
404
415
|
|
|
405
416
|
function resolveWindowsRuntimeRoot(identity: Extract<UserIdentity, { platform: "win32" }>): string {
|
|
406
417
|
if (!SID_PATTERN.test(identity.sid)) refuse("The coordinator identity contains an invalid SID.");
|
|
407
|
-
const localAppData =
|
|
418
|
+
const localAppData = windowsLocalAppData();
|
|
408
419
|
if (!isAbsolute(localAppData)) refuse("Windows LocalAppData resolution returned a relative path.");
|
|
409
420
|
|
|
410
|
-
// The SID and known-folder values come from
|
|
421
|
+
// The SID and known-folder values come from effective-token OS APIs,
|
|
411
422
|
// never USERPROFILE or LOCALAPPDATA. WP11 adds descriptor/reparse/ACL checks at
|
|
412
423
|
// the stable-database open boundary where those checks can cover SQLite too.
|
|
413
424
|
const root = resolve(localAppData, "OpenCodex", "Runtime", "v1", identity.sid.toUpperCase());
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAuthorizedProviderExecutedCall, type ProviderExecutedCallType } from "../server/responses-undeclared-tool-guard";
|
|
3
|
+
|
|
4
|
+
// Logical recency avoids clock adjustments and ties between messages in one millisecond.
|
|
5
|
+
let activityOrder = 0;
|
|
6
|
+
const MAX_PENDING_RESULTS = 1024;
|
|
7
|
+
const MAX_RESULT_ID_LENGTH = 1024;
|
|
8
|
+
const resultTypes: Readonly<Record<string, string>> = {
|
|
9
|
+
function_call: "function_call_output",
|
|
10
|
+
custom_tool_call: "custom_tool_call_output",
|
|
11
|
+
local_shell_call: "function_call_output",
|
|
12
|
+
shell_call: "shell_call_output",
|
|
13
|
+
computer_call: "computer_call_output",
|
|
14
|
+
apply_patch_call: "apply_patch_call_output",
|
|
15
|
+
tool_search_call: "tool_search_output",
|
|
16
|
+
mcp_approval_request: "mcp_approval_response",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resultKey(type: string, id: string): string {
|
|
24
|
+
return createHash("sha256").update(JSON.stringify([type, id])).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Content-free activity owned by one admitted incoming Responses socket. */
|
|
28
|
+
export class CodexWebSocketActivity {
|
|
29
|
+
lastUsed = ++activityOrder;
|
|
30
|
+
closing = false;
|
|
31
|
+
private turns = 0;
|
|
32
|
+
private pendingResults = new Set<string>();
|
|
33
|
+
private returnedResults = new Set<string>();
|
|
34
|
+
// Hashed identities only; null marks a route-authorized provider-owned call.
|
|
35
|
+
private argumentCalls = new Map<string, { type: string; result: string | null }>();
|
|
36
|
+
// Unknown/oversized identities cannot safely prove that client work finished.
|
|
37
|
+
private untrackedResults = false;
|
|
38
|
+
|
|
39
|
+
get idle(): boolean {
|
|
40
|
+
return !this.closing && this.turns === 0 && this.pendingResults.size === 0 && !this.untrackedResults;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
touch(): void {
|
|
44
|
+
this.lastUsed = ++activityOrder;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Each turn retains its own busy reference, including superseded async cleanup. */
|
|
48
|
+
beginTurn(input: unknown): () => void {
|
|
49
|
+
this.turns += 1;
|
|
50
|
+
this.touch();
|
|
51
|
+
this.acceptResults(input);
|
|
52
|
+
// Ordinary creates start a new response even when its upstream omits response.created.
|
|
53
|
+
// Prior results settle old waits, but cannot suppress new calls reusing their IDs.
|
|
54
|
+
this.returnedResults.clear();
|
|
55
|
+
this.argumentCalls.clear();
|
|
56
|
+
let finished = false;
|
|
57
|
+
return () => {
|
|
58
|
+
if (finished) return;
|
|
59
|
+
finished = true;
|
|
60
|
+
this.turns -= 1;
|
|
61
|
+
if (this.turns === 0) this.touch();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Called only after admission or native control accepts the client's results. */
|
|
66
|
+
acceptResults(input: unknown): void {
|
|
67
|
+
if (this.untrackedResults) return;
|
|
68
|
+
if (Array.isArray(input)) {
|
|
69
|
+
for (const item of input) {
|
|
70
|
+
if (!record(item) || typeof item.type !== "string") continue;
|
|
71
|
+
const id = item.type === "mcp_approval_response" ? item.approval_request_id : item.call_id;
|
|
72
|
+
if (typeof id === "string" && id.length <= MAX_RESULT_ID_LENGTH) {
|
|
73
|
+
// Ordinary Codex shell results use function_call_output; native steering
|
|
74
|
+
// also accepts its explicit local_shell_call_output continuation stub.
|
|
75
|
+
const type = item.type === "local_shell_call_output" ? "function_call_output" : item.type;
|
|
76
|
+
const key = resultKey(type, id);
|
|
77
|
+
if (this.pendingResults.delete(key)) {
|
|
78
|
+
if (this.returnedResults.size >= MAX_PENDING_RESULTS) { this.protectUntracked(); return; }
|
|
79
|
+
else this.returnedResults.add(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Preserve tool waits even when the response terminal omits its output array. */
|
|
87
|
+
observePayload(payload: string, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
|
|
88
|
+
if (this.untrackedResults) return;
|
|
89
|
+
let frame: unknown;
|
|
90
|
+
try { frame = JSON.parse(payload); } catch { return; }
|
|
91
|
+
if (!record(frame)) return;
|
|
92
|
+
// Native injection can return a result before the same terminal repeats its call.
|
|
93
|
+
if (frame.type === "response.created") {
|
|
94
|
+
this.returnedResults.clear();
|
|
95
|
+
this.argumentCalls.clear();
|
|
96
|
+
}
|
|
97
|
+
if (frame.type === "response.output_item.added") this.rememberArgumentCall(frame.item, providerExecuted);
|
|
98
|
+
if (frame.type === "response.function_call_arguments.done" || frame.type === "response.custom_tool_call_input.done") {
|
|
99
|
+
const type = frame.type === "response.function_call_arguments.done" ? "function_call" : "custom_tool_call";
|
|
100
|
+
const call = typeof frame.item_id === "string" && frame.item_id.length <= MAX_RESULT_ID_LENGTH
|
|
101
|
+
? this.argumentCalls.get(resultKey("item", frame.item_id)) : undefined;
|
|
102
|
+
if (!call || call.type !== type) this.protectUntracked();
|
|
103
|
+
else if (call.result !== null) this.observeResultKey(call.result);
|
|
104
|
+
}
|
|
105
|
+
if (frame.type === "response.output_item.done") this.observeOutput(frame.item, providerExecuted);
|
|
106
|
+
if (["response.completed", "response.incomplete", "response.failed"].includes(String(frame.type))
|
|
107
|
+
&& record(frame.response) && Array.isArray(frame.response.output)) {
|
|
108
|
+
for (const item of frame.response.output) this.observeOutput(item, providerExecuted);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private rememberArgumentCall(item: unknown, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
|
|
113
|
+
if (!record(item) || (item.type !== "function_call" && item.type !== "custom_tool_call")) return;
|
|
114
|
+
if (typeof item.id !== "string" || !item.id.length || item.id.length > MAX_RESULT_ID_LENGTH) return;
|
|
115
|
+
const key = resultKey("item", item.id);
|
|
116
|
+
const hosted = providerExecuted && isAuthorizedProviderExecutedCall(item, providerExecuted);
|
|
117
|
+
if (!hosted && (typeof item.call_id !== "string" || !item.call_id.length || item.call_id.length > MAX_RESULT_ID_LENGTH)) {
|
|
118
|
+
if (this.argumentCalls.has(key)) this.protectUntracked();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const result = hosted ? null : resultKey(resultTypes[item.type]!, item.call_id as string);
|
|
122
|
+
const previous = this.argumentCalls.get(key);
|
|
123
|
+
if (previous) {
|
|
124
|
+
if (previous.type !== item.type || previous.result !== result) this.protectUntracked();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (this.argumentCalls.size >= MAX_PENDING_RESULTS) { this.protectUntracked(); return; }
|
|
128
|
+
// An unfinished argument stream alone does not prove the client started work.
|
|
129
|
+
this.argumentCalls.set(key, { type: item.type, result });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private observeOutput(item: unknown, providerExecuted?: ReadonlySet<ProviderExecutedCallType>): void {
|
|
133
|
+
if (this.untrackedResults) return;
|
|
134
|
+
if (!record(item) || typeof item.type !== "string") return;
|
|
135
|
+
// Hosted execution may publish both the call and its result in the output stream.
|
|
136
|
+
this.acceptResults([item]);
|
|
137
|
+
if (!Object.hasOwn(resultTypes, item.type)) return;
|
|
138
|
+
if (providerExecuted && isAuthorizedProviderExecutedCall(item, providerExecuted)) return;
|
|
139
|
+
if (item.type === "tool_search_call" && item.execution !== "client") return;
|
|
140
|
+
const id = item.type === "mcp_approval_request" ? item.id
|
|
141
|
+
: item.type === "local_shell_call" || item.type === "tool_search_call" ? item.call_id ?? item.id : item.call_id;
|
|
142
|
+
if (typeof id !== "string" || !id.length || id.length > MAX_RESULT_ID_LENGTH) {
|
|
143
|
+
this.protectUntracked();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.observeResultKey(resultKey(resultTypes[item.type]!, id));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private observeResultKey(key: string): void {
|
|
150
|
+
if (this.pendingResults.has(key) || this.returnedResults.has(key)) return;
|
|
151
|
+
if (this.pendingResults.size >= MAX_PENDING_RESULTS) this.protectUntracked();
|
|
152
|
+
else this.pendingResults.add(key);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private protectUntracked(): void {
|
|
156
|
+
this.untrackedResults = true;
|
|
157
|
+
this.pendingResults.clear();
|
|
158
|
+
this.returnedResults.clear();
|
|
159
|
+
this.argumentCalls.clear();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
import type { ServerWebSocket } from "bun";
|
|
2
2
|
import type { WsData } from "../server/ws-bridge";
|
|
3
3
|
import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
|
|
4
|
+
import { CodexWebSocketActivity } from "./websocket-activity";
|
|
4
5
|
|
|
5
6
|
export const MAX_TRACKED_CODEX_WEBSOCKETS = 128;
|
|
6
7
|
const websocketGate = createAdmissionGate("codex_websockets", MAX_TRACKED_CODEX_WEBSOCKETS);
|
|
8
|
+
const admittedSockets = new Set<ServerWebSocket<WsData>>();
|
|
9
|
+
|
|
10
|
+
function reclaimOldestIdleSocket(): void {
|
|
11
|
+
let oldest: ServerWebSocket<WsData> | undefined;
|
|
12
|
+
for (const ws of admittedSockets) {
|
|
13
|
+
const activity = ws.data.responsesActivity;
|
|
14
|
+
if (!activity?.idle || ws.data.nativeControl || ws.readyState !== 1 || ws.getBufferedAmount() !== 0) continue;
|
|
15
|
+
if (!oldest || activity.lastUsed < oldest.data.responsesActivity!.lastUsed) oldest = ws;
|
|
16
|
+
}
|
|
17
|
+
if (!oldest) return;
|
|
18
|
+
// Fence late frames before closing. Only the actual close callback releases admission.
|
|
19
|
+
oldest.data.responsesActivity!.closing = true;
|
|
20
|
+
try {
|
|
21
|
+
oldest.close(1001, "Idle Responses connection reclaimed");
|
|
22
|
+
} catch {
|
|
23
|
+
// A failed/delayed close never manufactures a free slot or retries another victim.
|
|
24
|
+
if (oldest.readyState === 1) oldest.data.responsesActivity!.closing = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
export function tryReserveCodexWebSocket(): AdmissionReservation<ServerWebSocket<WsData>> | null {
|
|
29
|
+
if (websocketGate.metrics().active >= MAX_TRACKED_CODEX_WEBSOCKETS) reclaimOldestIdleSocket();
|
|
9
30
|
const gateLease = websocketGate.tryAcquire();
|
|
10
31
|
if (!gateLease) return null;
|
|
11
32
|
let socket: ServerWebSocket<WsData> | undefined;
|
|
12
33
|
let active = true;
|
|
13
34
|
return {
|
|
14
35
|
bind(value) {
|
|
15
|
-
if (!active) return;
|
|
36
|
+
if (!active || socket) return;
|
|
16
37
|
socket = value;
|
|
38
|
+
socket.data.responsesActivity = new CodexWebSocketActivity();
|
|
39
|
+
admittedSockets.add(socket);
|
|
17
40
|
},
|
|
18
41
|
release() {
|
|
19
42
|
if (!active) return;
|
|
20
43
|
active = false;
|
|
44
|
+
if (socket) admittedSockets.delete(socket);
|
|
21
45
|
socket = undefined;
|
|
22
46
|
gateLease.release();
|
|
23
47
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { dlopen, ptr, read, type Pointer } from "bun:ffi";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { resolveTrustedWindowsSystemDirectory } from "../lib/windows-elevation";
|
|
4
|
+
|
|
5
|
+
// FOLDERID_LocalAppData: GUID fields Data1/Data2/Data3 are little-endian.
|
|
6
|
+
const LOCAL_APPDATA_ID = Buffer.from("8527b3f1ba6fcf4f9d557b8e7f157091", "hex");
|
|
7
|
+
const KF_FLAG_DEFAULT_PATH = 0x400;
|
|
8
|
+
const MAX_PATH_CODE_UNITS = 32_768;
|
|
9
|
+
|
|
10
|
+
export interface WindowsKnownFolderApi {
|
|
11
|
+
getPath(id: Uint8Array, flags: number, token: null, output: BigUint64Array): number;
|
|
12
|
+
readCodeUnit(address: number, byteOffset: number): number;
|
|
13
|
+
free(address: number): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Same current-token/default-path API as the PowerShell fallback, without Add-Type. */
|
|
17
|
+
export function readWindowsLocalAppData(api: WindowsKnownFolderApi): string {
|
|
18
|
+
const output = new BigUint64Array(1);
|
|
19
|
+
let address = 0;
|
|
20
|
+
try {
|
|
21
|
+
const hr = api.getPath(LOCAL_APPDATA_ID, KF_FLAG_DEFAULT_PATH, null, output);
|
|
22
|
+
address = Number(output[0]);
|
|
23
|
+
if (hr !== 0 || !Number.isSafeInteger(address) || address <= 0) {
|
|
24
|
+
throw new Error("Windows LocalAppData known-folder lookup failed.");
|
|
25
|
+
}
|
|
26
|
+
const units: number[] = [];
|
|
27
|
+
for (let index = 0; index < MAX_PATH_CODE_UNITS; index += 1) {
|
|
28
|
+
const unit = api.readCodeUnit(address, index * 2);
|
|
29
|
+
if (unit === 0) {
|
|
30
|
+
const path = String.fromCharCode(...units);
|
|
31
|
+
if (!win32.isAbsolute(path) || !/^[a-z]:\\|^\\\\/i.test(path)) {
|
|
32
|
+
throw new Error("Windows LocalAppData known-folder lookup returned an invalid path.");
|
|
33
|
+
}
|
|
34
|
+
return path;
|
|
35
|
+
}
|
|
36
|
+
units.push(unit);
|
|
37
|
+
}
|
|
38
|
+
throw new Error("Windows LocalAppData known-folder lookup returned an unterminated path.");
|
|
39
|
+
} finally {
|
|
40
|
+
// SHGetKnownFolderPath requires CoTaskMemFree even on a failed HRESULT.
|
|
41
|
+
if (address > 0 && Number.isSafeInteger(address)) api.free(address);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let nativeLookup: (() => string) | null | undefined;
|
|
46
|
+
|
|
47
|
+
function loadNativeLookup(): (() => string) | null {
|
|
48
|
+
if (nativeLookup !== undefined) return nativeLookup;
|
|
49
|
+
if (process.platform !== "win32" || process.arch !== "x64") return null;
|
|
50
|
+
try {
|
|
51
|
+
const systemDirectory = resolveTrustedWindowsSystemDirectory();
|
|
52
|
+
const shell = dlopen(win32.join(systemDirectory, "shell32.dll"), {
|
|
53
|
+
SHGetKnownFolderPath: { args: ["ptr", "u32", "ptr", "ptr"], returns: "i32" },
|
|
54
|
+
});
|
|
55
|
+
let ole;
|
|
56
|
+
try {
|
|
57
|
+
ole = dlopen(win32.join(systemDirectory, "ole32.dll"), {
|
|
58
|
+
CoTaskMemFree: { args: ["ptr"], returns: "void" },
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
shell.close();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const api: WindowsKnownFolderApi = {
|
|
65
|
+
getPath: (id, flags, token, output) => shell.symbols.SHGetKnownFolderPath(
|
|
66
|
+
ptr(id), flags, token, ptr(output),
|
|
67
|
+
),
|
|
68
|
+
readCodeUnit: (address, offset) => read.u16(address as Pointer, offset),
|
|
69
|
+
free: address => { ole.symbols.CoTaskMemFree(address as Pointer); },
|
|
70
|
+
};
|
|
71
|
+
nativeLookup = () => readWindowsLocalAppData(api);
|
|
72
|
+
} catch {
|
|
73
|
+
nativeLookup = null;
|
|
74
|
+
}
|
|
75
|
+
return nativeLookup;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Unsupported FFI keeps the bounded trusted helper; an OS API failure still refuses. */
|
|
79
|
+
export function resolveWindowsLocalAppData(
|
|
80
|
+
powershellLookup: () => string,
|
|
81
|
+
lookup: (() => string) | null = loadNativeLookup(),
|
|
82
|
+
): string {
|
|
83
|
+
return lookup ? lookup() : powershellLookup();
|
|
84
|
+
}
|
package/src/combos/failover.ts
CHANGED
|
@@ -403,6 +403,9 @@ const REQUEST_SHAPE_FAILURE_CODES = new Set([
|
|
|
403
403
|
"tool_catalog_too_large",
|
|
404
404
|
"cursor_root_envelope_limit",
|
|
405
405
|
"target_incompatible",
|
|
406
|
+
// A route that cannot read the request's media (image input to a text-only CLI route) is
|
|
407
|
+
// healthy for every other request, so cooling it would only block unrelated turns.
|
|
408
|
+
"unsupported_input_modality",
|
|
406
409
|
]);
|
|
407
410
|
|
|
408
411
|
/** Credential/billing failures that every target sharing the provider inherits. */
|
|
@@ -836,7 +839,9 @@ export function comboFailureDecision(
|
|
|
836
839
|
// says nothing about the next combo target. Structured code only, plus the explicit prose
|
|
837
840
|
// form upstreams emit when they carry no code, so an unrelated 400 stays terminal.
|
|
838
841
|
const failureCode = normalizedFailureCode(options?.code || error.code);
|
|
839
|
-
|
|
842
|
+
// The input-modality refusal is the same kind of verdict: this route cannot read the turn's
|
|
843
|
+
// media, and a later target may. Its 400 would otherwise end the chain as invalid_request_error.
|
|
844
|
+
if (["model_not_found", "model_unavailable", "unsupported_model", "unsupported_input_modality"].includes(failureCode)) {
|
|
840
845
|
return "hop";
|
|
841
846
|
}
|
|
842
847
|
if (modelRefusal === "refusal") return "hop";
|
package/src/combos/index.ts
CHANGED
package/src/combos/resolve.ts
CHANGED
|
@@ -55,17 +55,49 @@ export class UnknownComboError extends Error {
|
|
|
55
55
|
export class NoAvailableComboTargetsError extends Error {
|
|
56
56
|
readonly code = "combo_unavailable";
|
|
57
57
|
|
|
58
|
-
constructor(readonly comboId: string) {
|
|
59
|
-
super(`No available targets for combo: ${comboId}`);
|
|
58
|
+
constructor(readonly comboId: string, readonly reasons = "") {
|
|
59
|
+
super(`No available targets for combo: ${comboId}${reasons ? ` (${reasons})` : ""}`);
|
|
60
60
|
this.name = "NoAvailableComboTargetsError";
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function providerExclusionReason(config: OcxConfig, target: OcxComboTarget, now: number): string | undefined {
|
|
65
|
+
const provider = Object.hasOwn(config.providers, target.provider) ? config.providers[target.provider] : undefined;
|
|
66
|
+
if (!provider) return "provider missing";
|
|
67
|
+
if (provider.disabled === true) return "provider disabled";
|
|
68
|
+
return cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model)
|
|
69
|
+
? "quota exhausted"
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now: number): boolean {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
return providerExclusionReason(config, target, now) === undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Why each target of a combo cannot be picked right now, for a zero-attempt refusal. Only the
|
|
79
|
+
* configured provider/model and a coarse label are named, never account or request data.
|
|
80
|
+
*/
|
|
81
|
+
export function comboExclusionReasons(
|
|
82
|
+
config: OcxConfig,
|
|
83
|
+
comboId: string,
|
|
84
|
+
eligible?: (target: NormalizedComboTarget) => boolean,
|
|
85
|
+
now = Date.now(),
|
|
86
|
+
): string {
|
|
87
|
+
return (getCombo(config, comboId)?.targets ?? []).flatMap(target => {
|
|
88
|
+
let reason = providerExclusionReason(config, target, now);
|
|
89
|
+
const cooldown = reason ? undefined : earliestComboCooldown(comboId, [target], now);
|
|
90
|
+
if (cooldown) reason = `cooldown ${Math.max(1, Math.ceil((cooldown.expiry - now) / 1000))}s`;
|
|
91
|
+
if (!reason && eligible) {
|
|
92
|
+
try {
|
|
93
|
+
if (!eligible(target)) reason = "ineligible for this request";
|
|
94
|
+
} catch {
|
|
95
|
+
// An eligibility check can exhaust the translator budget; the target still was not picked.
|
|
96
|
+
reason = "ineligible for this request";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return reason ? [`${targetKey(target)}: ${reason}`] : [];
|
|
100
|
+
}).join("; ");
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
/** Capture usable providers' quota cooldowns; request eligibility is deferred to exhaustion. */
|
|
@@ -100,6 +132,9 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und
|
|
|
100
132
|
const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]);
|
|
101
133
|
|
|
102
134
|
function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean {
|
|
135
|
+
// Exact Antigravity model readings are diagnostic only. They are not permission or
|
|
136
|
+
// provider-wide exhaustion evidence until a separate eligibility contract consumes them.
|
|
137
|
+
if (window.modelId !== undefined) return false;
|
|
103
138
|
if (window.scope !== "model" || model === undefined) return true;
|
|
104
139
|
const family = window.label.trim().toLowerCase();
|
|
105
140
|
if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true;
|
|
@@ -408,9 +443,10 @@ export function advanceComboAfterFailure(
|
|
|
408
443
|
// including the pool's OWN "all accounts are cooled" 429, which refuses locally before it picks
|
|
409
444
|
// one. That is what keeps this from trading a blackhole for a hammer: when nothing else is
|
|
410
445
|
// holding the target back, the target cooldown still does.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
446
|
+
// A 402 credit refusal is the provider-scoped twin: the caller names an account only after the
|
|
447
|
+
// pool has cooled it and still holds an eligible sibling.
|
|
448
|
+
const accountScoped = options.failedAccount !== undefined
|
|
449
|
+
&& (options.status === 429 ? options.cooldownScope === "target" : options.status === 402);
|
|
414
450
|
// "none" records no cooldown at all: the failure described the request, not the target, so
|
|
415
451
|
// the target must stay immediately selectable for the next (differently shaped) request.
|
|
416
452
|
if (options.cooldownScope !== "none" && !accountScoped) {
|
|
@@ -612,6 +648,6 @@ export function tryPickComboModel(config: OcxConfig, modelId: string, preview =
|
|
|
612
648
|
if (!comboId) return null;
|
|
613
649
|
if (!getCombo(config, comboId)) throw new UnknownComboError(comboId);
|
|
614
650
|
const picked = pickComboTarget(config, comboId, { preview });
|
|
615
|
-
if (!picked) throw new NoAvailableComboTargetsError(comboId);
|
|
651
|
+
if (!picked) throw new NoAvailableComboTargetsError(comboId, comboExclusionReasons(config, comboId));
|
|
616
652
|
return picked;
|
|
617
653
|
}
|
package/src/combos/types.ts
CHANGED
|
@@ -39,12 +39,16 @@ export interface NormalizedComboTarget {
|
|
|
39
39
|
weight: number;
|
|
40
40
|
/** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
|
|
41
41
|
lastResort: boolean;
|
|
42
|
+
/** Pay-per-use target; requires key consent or loopback-only operator consent. */
|
|
43
|
+
metered: boolean;
|
|
42
44
|
reasoningEfforts?: OcxComboDefaultEffort[];
|
|
43
45
|
/** Optional JEV decision description. */
|
|
44
46
|
modelProfile?: string;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface NormalizedComboConfig {
|
|
50
|
+
/** Operator consent, effective only on a loopback-only proxy. */
|
|
51
|
+
allowMeteredTargets?: boolean;
|
|
48
52
|
strategy: OcxComboStrategy;
|
|
49
53
|
stickyLimit: number;
|
|
50
54
|
cooldownMs?: number;
|
|
@@ -164,6 +168,9 @@ export function comboConfigIssues(
|
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
const body = raw as Record<string, unknown>;
|
|
171
|
+
if (body.allowMeteredTargets !== undefined && typeof body.allowMeteredTargets !== "boolean") {
|
|
172
|
+
issues.push({ path: ["allowMeteredTargets"], message: "allowMeteredTargets must be a boolean" });
|
|
173
|
+
}
|
|
167
174
|
if (typeof body.alias === "string") {
|
|
168
175
|
const alias = body.alias.toLowerCase();
|
|
169
176
|
for (const [providerName, provider] of Object.entries(providers)) {
|
|
@@ -427,6 +434,12 @@ export function comboConfigIssues(
|
|
|
427
434
|
message: `targets[${i}].lastResort must be a boolean`,
|
|
428
435
|
});
|
|
429
436
|
}
|
|
437
|
+
if (target.metered !== undefined && typeof target.metered !== "boolean") {
|
|
438
|
+
issues.push({
|
|
439
|
+
path: ["targets", i, "metered"],
|
|
440
|
+
message: `targets[${i}].metered must be a boolean`,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
430
443
|
if (target.modelProfile !== undefined
|
|
431
444
|
&& (typeof target.modelProfile !== "string"
|
|
432
445
|
|| target.modelProfile.trim().length === 0
|
|
@@ -477,6 +490,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
477
490
|
: null;
|
|
478
491
|
return {
|
|
479
492
|
strategy: raw.strategy ?? "failover",
|
|
493
|
+
...(raw.allowMeteredTargets === true ? { allowMeteredTargets: true } : {}),
|
|
480
494
|
stickyLimit: raw.stickyLimit ?? 1,
|
|
481
495
|
cooldownMs: raw.cooldownMs,
|
|
482
496
|
waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
|
|
@@ -503,6 +517,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
503
517
|
? { modelProfile: target.modelProfile.trim() }
|
|
504
518
|
: {}),
|
|
505
519
|
lastResort: target.lastResort === true,
|
|
520
|
+
metered: target.metered === true,
|
|
506
521
|
})),
|
|
507
522
|
};
|
|
508
523
|
}
|