@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mutatePersistedConfig, readConfigAdmissionSnapshot, withConfigMutationLockSync } from "../config";
|
|
2
|
+
import { GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount } from "../lib/gcp-adc";
|
|
3
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
4
|
+
import { isKeychainReference, keychainAccount, providerKeychainEntry } from "./api-key-resolve";
|
|
5
|
+
|
|
6
|
+
/** Snapshot credential references before removing a key or provider. */
|
|
7
|
+
export function providerGcpCredentialMarkers(provider: OcxProviderConfig): string[] {
|
|
8
|
+
return [...new Set([provider.apiKey, ...(provider.apiKeyPool ?? []).map(entry => entry.key)]
|
|
9
|
+
.filter((key): key is string => typeof key === "string" && key.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)))];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Capture both the caller's row and the valid durable row before a provider mutation. */
|
|
13
|
+
export function snapshotProviderGcpCredentialMarkers(name: string, provider: OcxProviderConfig | undefined): string[] {
|
|
14
|
+
const durable = readConfigAdmissionSnapshot();
|
|
15
|
+
const durableProvider = durable.kind === "read" && durable.diagnostics.source === "file"
|
|
16
|
+
&& durable.diagnostics.error === null ? durable.diagnostics.config.providers[name] : undefined;
|
|
17
|
+
return [provider, durableProvider].flatMap(row => row ? providerGcpCredentialMarkers(row) : []);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Capture deletion candidates and save under the same lock; cleanup follows a successful save. */
|
|
21
|
+
export function commitProviderGcpCredentialRemoval<T>(config: OcxConfig, name: string, saveRemoval: () => T): T {
|
|
22
|
+
let markers: string[] = [];
|
|
23
|
+
const result = withConfigMutationLockSync(() => {
|
|
24
|
+
markers = snapshotProviderGcpCredentialMarkers(name, config.providers[name]);
|
|
25
|
+
return saveRemoval();
|
|
26
|
+
});
|
|
27
|
+
cleanupRemovedGcpCredentials(markers);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanupGcpCredentialAccount(key: string | undefined): string | undefined {
|
|
32
|
+
if (key?.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) && !key.slice(GCP_CREDENTIAL_MARKER_PREFIX.length).trim()) return undefined;
|
|
33
|
+
return gcpCredentialMarkerAccount(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** After a successful config commit, remove only credentials no persisted provider references. */
|
|
37
|
+
export function cleanupRemovedGcpCredentials(markers: readonly string[]): void {
|
|
38
|
+
if (!markers.length) return;
|
|
39
|
+
try {
|
|
40
|
+
// Re-read under the config mutation lock: another provider may share a marker, or a newer
|
|
41
|
+
// commit may have reintroduced it. Never decide deletion from the caller's stale snapshot.
|
|
42
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
43
|
+
const retained = new Set(Object.values(fresh.providers).flatMap(provider =>
|
|
44
|
+
[provider.apiKey, ...(provider.apiKeyPool ?? []).map(entry => entry.key)]
|
|
45
|
+
.flatMap(key => {
|
|
46
|
+
const account = isKeychainReference(key) ? keychainAccount(key) : cleanupGcpCredentialAccount(key);
|
|
47
|
+
return account === undefined ? [] : [account];
|
|
48
|
+
})));
|
|
49
|
+
for (const marker of new Set(markers)) {
|
|
50
|
+
const account = cleanupGcpCredentialAccount(marker);
|
|
51
|
+
if (account && !retained.has(account)) providerKeychainEntry(account).deletePassword();
|
|
52
|
+
}
|
|
53
|
+
return { changed: false, value: true };
|
|
54
|
+
});
|
|
55
|
+
if (outcome.status === "unavailable") throw new Error("configuration unavailable");
|
|
56
|
+
} catch {
|
|
57
|
+
console.warn("GCP credential cleanup incomplete; inspect the OS credential store. Configuration removal remains committed.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
10
|
*/
|
|
11
|
+
import { isKeyInCooldown, getKeyCooldownUntil, setKeyCooldown } from "./api-key-cooldowns";
|
|
12
|
+
export { clearKeyCooldowns, getKeyCooldownUntil, sweepExpiredApiKeyCooldowns } from "./api-key-cooldowns";
|
|
11
13
|
import { commitProviderApiKeySelection, providerApiKeySelectionIsCurrent } from "./api-key-selection";
|
|
14
|
+
import { RequestPacingQueueOverloadError } from "./request-pacing";
|
|
12
15
|
import type { ProviderApiKeySelection } from "../types/provider";
|
|
13
|
-
import { routedProviderConfig } from "
|
|
16
|
+
import { routedProviderConfig } from "./routed-config";
|
|
14
17
|
import { getProviderRegistryEntry } from "./registry";
|
|
15
18
|
import { isCanonicalCommandCodeBaseUrl, normalizedBaseUrl } from "./quota/vendor-probes-key";
|
|
16
19
|
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, ResetReplayPolicy, TransientRetryPolicy } from "../types";
|
|
@@ -24,10 +27,6 @@ import { cachedApiKeyQuota } from "./quota-key-accounts";
|
|
|
24
27
|
|
|
25
28
|
// ---- cooldown state (in-memory, same as codex/routing.ts) ----
|
|
26
29
|
|
|
27
|
-
interface KeyCooldown {
|
|
28
|
-
cooldownUntil: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
30
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
32
31
|
const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
|
|
33
32
|
/**
|
|
@@ -347,11 +346,6 @@ const DEFAULT_RESET_REPLAY = {
|
|
|
347
346
|
} as const satisfies Required<ResetReplayPolicy>;
|
|
348
347
|
|
|
349
348
|
/** Map<`${providerName}\0${keyId}`, KeyCooldown> */
|
|
350
|
-
const keyCooldowns = new Map<string, KeyCooldown>();
|
|
351
|
-
|
|
352
|
-
function cooldownKey(providerName: string, keyId: string): string {
|
|
353
|
-
return `${providerName}\0${keyId}`;
|
|
354
|
-
}
|
|
355
349
|
|
|
356
350
|
/**
|
|
357
351
|
* Parse an upstream `Retry-After` header: numeric seconds (including `0`) or an HTTP-date.
|
|
@@ -379,16 +373,6 @@ function parseRetryAfterMs(value: string | null | undefined, now = Date.now()):
|
|
|
379
373
|
* True while the given key is inside its 429 cooldown window (lazily evicting the entry once the
|
|
380
374
|
* window expires). Used to skip keys that the upstream just rate-limited during failover.
|
|
381
375
|
*/
|
|
382
|
-
function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
|
|
383
|
-
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
384
|
-
if (!entry) return false;
|
|
385
|
-
if (entry.cooldownUntil <= now) {
|
|
386
|
-
keyCooldowns.delete(cooldownKey(providerName, keyId));
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
376
|
// ---- public API ----
|
|
393
377
|
|
|
394
378
|
/**
|
|
@@ -418,9 +402,11 @@ const keyRotationCursor = new Map<string, string>();
|
|
|
418
402
|
export function forgetApiKeyRotationCursor(providerName?: string): void {
|
|
419
403
|
if (!providerName) {
|
|
420
404
|
keyRotationCursor.clear();
|
|
405
|
+
dispatchKeyCursor.clear();
|
|
421
406
|
return;
|
|
422
407
|
}
|
|
423
408
|
keyRotationCursor.delete(providerName);
|
|
409
|
+
dispatchKeyCursor.delete(providerName);
|
|
424
410
|
}
|
|
425
411
|
|
|
426
412
|
/** The pool entry shape is inline on OcxProviderConfig; name it once rather than re-spelling it. */
|
|
@@ -566,11 +552,33 @@ export function selectProactiveApiKeyTransport(
|
|
|
566
552
|
promptCacheKey?: string,
|
|
567
553
|
now = Date.now(),
|
|
568
554
|
): OcxProviderTransport | null {
|
|
555
|
+
const configured = config.providers[providerName];
|
|
556
|
+
if (configured?.requestPacing?.enabled && configured.requestPacing.keySelection === "round-robin"
|
|
557
|
+
&& hasKeyPoolFailover(configured)) {
|
|
558
|
+
return applyRotatedTransport(providerName, routedProvider, allocateDistributedKey(providerName, configured, now), promptCacheKey);
|
|
559
|
+
}
|
|
569
560
|
const committed = selectProactiveApiKey(config, providerName, now);
|
|
570
561
|
if (!committed) return null;
|
|
571
562
|
return applyRotatedTransport(providerName, routedProvider, committed, promptCacheKey);
|
|
572
563
|
}
|
|
573
564
|
|
|
565
|
+
const dispatchKeyCursor = new Map<string, string>();
|
|
566
|
+
function allocateDistributedKey(providerName: string, provider: OcxProviderConfig, now: number): OcxProviderConfig {
|
|
567
|
+
const pool = (provider.apiKeyPool ?? []).filter((entry, index, all) => all.findIndex(other => other.key === entry.key) === index);
|
|
568
|
+
const previous = dispatchKeyCursor.get(providerName);
|
|
569
|
+
const start = pool.findIndex(entry => entry.id === previous);
|
|
570
|
+
for (let offset = 1; offset <= pool.length; offset++) {
|
|
571
|
+
const entry = pool[(start + offset) % pool.length]!;
|
|
572
|
+
if (isKeyInCooldown(providerName, entry.id, now)) continue;
|
|
573
|
+
dispatchKeyCursor.set(providerName, entry.id);
|
|
574
|
+
return { ...provider, apiKey: entry.key, _apiKeyAttempt: {
|
|
575
|
+
entryId: entry.id, reference: entry.key, revision: provider.apiKeySelectionRevision, distributed: true,
|
|
576
|
+
} };
|
|
577
|
+
}
|
|
578
|
+
const next = Math.min(...pool.map(entry => getKeyCooldownUntil(providerName, entry.id, now) ?? now + 1000));
|
|
579
|
+
throw new RequestPacingQueueOverloadError(providerName, "keys_exhausted", Math.max(1, Math.ceil((next - now) / 1000)));
|
|
580
|
+
}
|
|
581
|
+
|
|
574
582
|
/**
|
|
575
583
|
* Normalize a provider's `retryOn429` policy. An explicit object always wins (including
|
|
576
584
|
* `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
|
|
@@ -695,6 +703,24 @@ export function rateLimitRetryDelayMs(
|
|
|
695
703
|
* rebuilds from this committed row, reapplies registry metadata, and retains only explicit
|
|
696
704
|
* runtime transport state (`fetch` and generated OpenCode session affinity).
|
|
697
705
|
*/
|
|
706
|
+
/** Record a request-local key failure without selecting or sending a replacement. */
|
|
707
|
+
export function recordDistributedKeyFailure(
|
|
708
|
+
config: OcxConfig, providerName: string, selection: ProviderApiKeySelection | undefined,
|
|
709
|
+
status: 401 | 429 | "billing", now = Date.now(), retryAfter?: string | null, quotaResetAt?: number,
|
|
710
|
+
): boolean {
|
|
711
|
+
const provider = config.providers[providerName];
|
|
712
|
+
if (!selection?.distributed || !provider || provider.authMode === "oauth" || provider.authMode === "forward") return false;
|
|
713
|
+
const failed = provider.apiKeyPool?.find(entry => entry.id === selection.entryId && entry.key === selection.reference);
|
|
714
|
+
if (!failed || provider.apiKeySelectionRevision !== selection.revision) return false;
|
|
715
|
+
const delay = status === "billing" ? ANTHROPIC_BILLING_COOLDOWN_MS : status === 401 ? MAX_COOLDOWN_MS : quotaResetAt !== undefined
|
|
716
|
+
? Math.max(1, quotaResetAt - now) : parseRetryAfterMs(retryAfter, now) ?? DEFAULT_COOLDOWN_MS;
|
|
717
|
+
for (const entry of provider.apiKeyPool ?? []) if (entry.key === failed.key) {
|
|
718
|
+
setKeyCooldown(providerName, entry.id, now + delay);
|
|
719
|
+
}
|
|
720
|
+
sweepExpiredOnWrite(now);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
698
724
|
function rotateKeyAfterFailure(
|
|
699
725
|
config: OcxConfig,
|
|
700
726
|
providerName: string,
|
|
@@ -710,6 +736,13 @@ function rotateKeyAfterFailure(
|
|
|
710
736
|
if (!provider) return null;
|
|
711
737
|
if (provider.authMode === "oauth" || provider.authMode === "forward") return null;
|
|
712
738
|
|
|
739
|
+
if (attemptedSelection?.distributed) {
|
|
740
|
+
if (acceptsFreshProvider && !acceptsFreshProvider(provider)) return null;
|
|
741
|
+
if (!recordDistributedKeyFailure(config, providerName, attemptedSelection, failureStatus, now, retryAfterHeader, quotaResetAt)) return null;
|
|
742
|
+
if (!(provider.apiKeyPool ?? []).some(entry => !isKeyInCooldown(providerName, entry.id, now))) return null;
|
|
743
|
+
return allocateDistributedKey(providerName, provider, now);
|
|
744
|
+
}
|
|
745
|
+
|
|
713
746
|
const failedKey = attemptedSelection?.reference ?? attemptedKey ?? provider.apiKey;
|
|
714
747
|
type Rotation =
|
|
715
748
|
| { failedId?: string; candidateId?: string }
|
|
@@ -778,7 +811,7 @@ function rotateKeyAfterFailure(
|
|
|
778
811
|
: quotaResetAt !== undefined
|
|
779
812
|
? Math.max(quotaResetAt - now, 1)
|
|
780
813
|
: parseRetryAfterMs(retryAfterHeader, now) ?? DEFAULT_COOLDOWN_MS;
|
|
781
|
-
|
|
814
|
+
setKeyCooldown(providerName, outcome.value.failedId, now + cooldownMs);
|
|
782
815
|
sweepExpiredOnWrite(now);
|
|
783
816
|
}
|
|
784
817
|
if ("exhaustedCount" in outcome.value) {
|
|
@@ -831,16 +864,6 @@ export function rotateKeyOn401(
|
|
|
831
864
|
return rotateKeyAfterFailure(config, providerName, 401, null, now, attemptedKey, attemptedSelection);
|
|
832
865
|
}
|
|
833
866
|
|
|
834
|
-
export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
|
|
835
|
-
let removed = 0;
|
|
836
|
-
for (const [key, cooldown] of keyCooldowns) {
|
|
837
|
-
if (cooldown.cooldownUntil > now) continue;
|
|
838
|
-
keyCooldowns.delete(key);
|
|
839
|
-
removed += 1;
|
|
840
|
-
}
|
|
841
|
-
return removed;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
867
|
interface RotateProviderTransportOptions {
|
|
845
868
|
retryAfter?: string | null;
|
|
846
869
|
/** Epoch ms from `parseQuotaResetAt`, when the upstream dated the reset in its body. */
|
|
@@ -934,22 +957,3 @@ function applyRotatedTransport(
|
|
|
934
957
|
};
|
|
935
958
|
return resolveProviderTransport(providerName, retryProvider, promptCacheKey);
|
|
936
959
|
}
|
|
937
|
-
|
|
938
|
-
/** Clear cooldown state for a provider (e.g. after manual key management). */
|
|
939
|
-
export function clearKeyCooldowns(providerName?: string): void {
|
|
940
|
-
if (!providerName) {
|
|
941
|
-
keyCooldowns.clear();
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const prefix = `${providerName}\0`;
|
|
945
|
-
for (const key of keyCooldowns.keys()) {
|
|
946
|
-
if (key.startsWith(prefix)) keyCooldowns.delete(key);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/** Visible-for-testing: get the cooldown-until timestamp for a key. */
|
|
951
|
-
export function getKeyCooldownUntil(providerName: string, keyId: string, now = Date.now()): number | null {
|
|
952
|
-
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
953
|
-
if (!entry) return null;
|
|
954
|
-
return entry.cooldownUntil > now ? entry.cooldownUntil : null;
|
|
955
|
-
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { resolveEnvValue, saveConfigPreservingClaudeCode } from "../config";
|
|
2
2
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
3
3
|
import type { ProviderRegistryEntry } from "./registry";
|
|
4
|
+
import { isGcpCredentialMaterial } from "../lib/gcp-adc";
|
|
4
5
|
import {
|
|
5
6
|
KEYCHAIN_REFERENCE_PREFIX,
|
|
7
|
+
GCP_CREDENTIAL_MARKER_PREFIX,
|
|
6
8
|
invalidateResolvedProviderKeyCache,
|
|
7
9
|
isKeychainReference,
|
|
8
10
|
keychainAccount,
|
|
@@ -79,7 +81,7 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
|
|
|
79
81
|
const pool = provider.apiKeyPool ?? [];
|
|
80
82
|
try {
|
|
81
83
|
for (const entry of pool) {
|
|
82
|
-
if (isKeychainReference(entry.key)) continue;
|
|
84
|
+
if (entry.key.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) || isKeychainReference(entry.key)) continue;
|
|
83
85
|
const secret = resolveEnvValue(entry.key);
|
|
84
86
|
if (!secret) continue; // unresolved env reference stays as-is
|
|
85
87
|
const account = `${name}/${entry.id}`;
|
|
@@ -87,7 +89,7 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
|
|
|
87
89
|
written.push(account);
|
|
88
90
|
planned.push(() => { entry.key = `${KEYCHAIN_REFERENCE_PREFIX}${account}`; });
|
|
89
91
|
}
|
|
90
|
-
if (provider.apiKey && !isKeychainReference(provider.apiKey)) {
|
|
92
|
+
if (provider.apiKey && !provider.apiKey.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) && !isKeychainReference(provider.apiKey)) {
|
|
91
93
|
const active = pool.find(e => e.key === provider.apiKey || (isKeychainReference(e.key) && false));
|
|
92
94
|
const secret = resolveEnvValue(provider.apiKey);
|
|
93
95
|
if (secret) {
|
|
@@ -138,6 +140,9 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
|
|
|
138
140
|
let value: string | null = null;
|
|
139
141
|
try { value = providerKeychainEntry(account).getPassword(); } catch { value = null; }
|
|
140
142
|
if (!value) return { ok: false, error: `OS keychain has no readable secret for ${ref}; config left unchanged`, status: 503 };
|
|
143
|
+
if (isGcpCredentialMaterial(value)) {
|
|
144
|
+
return { ok: false, error: "GCP credentials must remain in the OS store; use a gcp-sa reference. Config left unchanged", status: 400 };
|
|
145
|
+
}
|
|
141
146
|
resolved.set(account, value);
|
|
142
147
|
}
|
|
143
148
|
for (const entry of pool) {
|
|
@@ -32,9 +32,12 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
|
|
|
32
32
|
preserveResponsesReasoningContent: "none",
|
|
33
33
|
preserveResponsesInputItemIds: "none",
|
|
34
34
|
preserveResponsesMessageMetadata: "none",
|
|
35
|
+
projectResponsesLiteHostedSearchHistory: "none",
|
|
35
36
|
modelReasoningEffortsAuthoritative: "none",
|
|
36
37
|
dropResponsesReasoningItems: "none",
|
|
37
38
|
decodesNativeCompactionBlobs: "none",
|
|
39
|
+
supportsNativeCompactionTrigger: "none",
|
|
40
|
+
supportsNativeCompactEndpoint: "none",
|
|
38
41
|
allowEncryptedV2AgentTasks: "none",
|
|
39
42
|
allowPrivateNetwork: "none",
|
|
40
43
|
proxy: "none",
|
|
@@ -292,8 +292,8 @@ function publishes(container: unknown, id: string): boolean {
|
|
|
292
292
|
*
|
|
293
293
|
* Under those two conditions the rewrite is worse than a no-op, because it never converges.
|
|
294
294
|
* `startServer` runs this migration and then `reconcileOAuthProviders`, and
|
|
295
|
-
* `applyOAuthPresetCatalog`
|
|
296
|
-
*
|
|
295
|
+
* `applyOAuthPresetCatalog` fills missing preset keys into the saved metadata maps.
|
|
296
|
+
* The deletion is undone inside the same boot, the config is written twice, and the user
|
|
297
297
|
* is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
|
|
298
298
|
* `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
|
|
299
299
|
* `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
|
|
@@ -67,19 +67,28 @@ function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): bool
|
|
|
67
67
|
/**
|
|
68
68
|
* Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
|
|
69
69
|
* backend can, and so can the official OpenAI API — but an arbitrary gateway that
|
|
70
|
-
* merely speaks the Responses wire cannot
|
|
71
|
-
*
|
|
70
|
+
* merely speaks the Responses wire cannot without an explicit capability opt-in (#422).
|
|
71
|
+
* This is a protocol claim, not OpenAI identity or permission to forward caller credentials.
|
|
72
72
|
*/
|
|
73
73
|
export function supportsNativeResponsesCompactEndpoint(
|
|
74
74
|
providerName: string,
|
|
75
75
|
provider: OcxProviderConfig,
|
|
76
76
|
): boolean {
|
|
77
77
|
if (isCanonicalOpenAiForwardProvider(provider)) return true;
|
|
78
|
+
// Generic OAuth selection/refresh belongs to the routed Responses path, not this direct transport.
|
|
79
|
+
if (provider.adapter === "openai-responses" && provider.supportsNativeCompactEndpoint === true
|
|
80
|
+
&& provider.authMode !== "oauth") return true;
|
|
78
81
|
return providerName === OPENAI_API_PROVIDER_ID
|
|
79
82
|
&& provider.adapter === "openai-responses"
|
|
80
83
|
&& normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/** Native v2 is independent of the v1 endpoint and ciphertext replay capabilities. */
|
|
87
|
+
export function supportsNativeResponsesCompactionTrigger(provider: OcxProviderConfig): boolean {
|
|
88
|
+
return isCanonicalOpenAiForwardProvider(provider)
|
|
89
|
+
|| (provider.adapter === "openai-responses" && provider.supportsNativeCompactionTrigger === true);
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
/**
|
|
84
93
|
* Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
|
|
85
94
|
* surface or the official OpenAI API.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
|
|
2
2
|
import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
|
|
3
3
|
import { openaiResponsesUrl } from "../adapters/openai-responses-url";
|
|
4
|
-
import {
|
|
4
|
+
import { COST4_RATE_KEYS, validCost4 } from "../usage/user-cost-validation";
|
|
5
5
|
import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers-destination";
|
|
6
|
-
export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
|
|
6
|
+
export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
|
|
7
7
|
|
|
8
8
|
const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
|
|
9
9
|
|
|
@@ -54,19 +54,12 @@ function managedLegacyMultiOverlay(
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/** Use the shared rate/policy contract while rejecting unknown legacy row fields. */
|
|
58
58
|
function validLegacyOverlayCosts(value: unknown): boolean {
|
|
59
59
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
60
|
-
const
|
|
60
|
+
const allowed = new Set<string>([...COST4_RATE_KEYS, "promptLengthPricing"]);
|
|
61
61
|
return Object.values(value as Record<string, unknown>).every(entry => {
|
|
62
|
-
|
|
63
|
-
const rates = entry as Record<string, unknown>;
|
|
64
|
-
return Object.keys(rates).length === fields.length
|
|
65
|
-
&& fields.every(key => Object.hasOwn(rates, key)
|
|
66
|
-
&& typeof rates[key] === "number"
|
|
67
|
-
&& Number.isFinite(rates[key])
|
|
68
|
-
&& (rates[key] as number) >= 0
|
|
69
|
-
&& (rates[key] as number) <= MAX_COST4_RATE);
|
|
62
|
+
return validCost4(entry) && Object.keys(entry).every(key => allowed.has(key));
|
|
70
63
|
});
|
|
71
64
|
}
|
|
72
65
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { OcxProviderConfig } from "../types";
|
|
3
3
|
import { registryEntryForProviderDestination } from "./registry";
|
|
4
|
+
import { isOpenCodeGoGeminiDestination } from "../types/opencode-go";
|
|
4
5
|
|
|
5
6
|
export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
|
|
6
7
|
|
|
@@ -44,7 +45,8 @@ export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
|
|
|
44
45
|
Pick<OcxProviderConfig, "baseUrl" | "adapter">
|
|
45
46
|
& Partial<Pick<OcxProviderConfig, "authMode">>,
|
|
46
47
|
): T {
|
|
47
|
-
if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go"
|
|
48
|
+
if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go"
|
|
49
|
+
&& !(provider.adapter === "google" && isOpenCodeGoGeminiDestination(provider))) return provider;
|
|
48
50
|
if (!sessionLane) return provider;
|
|
49
51
|
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
|
|
50
52
|
|
|
@@ -70,6 +70,9 @@ export function enrichOpenCodeZenRateLimitMessage(
|
|
|
70
70
|
message: string,
|
|
71
71
|
opts: {
|
|
72
72
|
status: number;
|
|
73
|
+
/** Parsed upstream error type, when available. */
|
|
74
|
+
upstreamErrorType?: string | null;
|
|
75
|
+
upstreamErrorCode?: string | null;
|
|
73
76
|
providerName?: string;
|
|
74
77
|
baseUrl?: string;
|
|
75
78
|
adapter?: string;
|
|
@@ -84,7 +87,9 @@ export function enrichOpenCodeZenRateLimitMessage(
|
|
|
84
87
|
): string {
|
|
85
88
|
if (opts.status !== 429) return message;
|
|
86
89
|
if (!isOpenCodeZenRateLimitProvider(opts)) return message;
|
|
87
|
-
|
|
90
|
+
const errorText = `${opts.upstreamErrorType ?? ""} ${opts.upstreamErrorCode ?? ""} ${message}`.replace(/[_-]/g, " ");
|
|
91
|
+
if (/\b(?:quota|billing)\b|\busage\s+limit\b|\binsufficient\s+(?:credits?|balance|funds)\b|\b(?:credits?|balance)\s+(?:exhausted|depleted)\b/i.test(errorText)) return message;
|
|
92
|
+
if (opts.upstreamErrorType !== "rate_limit_error" && !/rate\s*limit/i.test(message)) return message;
|
|
88
93
|
if (message.includes(ENRICHMENT_MARKER)) return message;
|
|
89
94
|
|
|
90
95
|
const upstreamRetry = validateClientRetryAfterHeader(
|
|
@@ -165,7 +170,7 @@ export function enrichOpenCodeZenFreeTierMessage(
|
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
/**
|
|
168
|
-
* Single entry point for Zen upstream-error guidance on
|
|
173
|
+
* Single entry point for Zen upstream-error guidance on Responses and native Messages: short-window
|
|
169
174
|
* rate limits first, then the keyless free-tier admission refusal. Each layer is a no-op
|
|
170
175
|
* outside its own case, so the composition is safe for every other upstream failure.
|
|
171
176
|
*/
|
|
@@ -200,7 +200,7 @@ export function supportsPerAccountQuota(provider: string): boolean {
|
|
|
200
200
|
|
|
201
201
|
export function explicitAccountReader(provider: string): boolean {
|
|
202
202
|
return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code"
|
|
203
|
-
|| provider === "devin";
|
|
203
|
+
|| provider === "devin" || provider === "dsh-account";
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
|
|
@@ -499,9 +499,9 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
|
|
|
499
499
|
const row = getAccountCredentialWithStatus(provider, accountId);
|
|
500
500
|
if (!row) throw new Error("account credential missing");
|
|
501
501
|
// An operator-paused account is excluded from every automatic upstream use, quota reads included.
|
|
502
|
-
// Anthropic instances also skip a needs-reauth row
|
|
502
|
+
// Anthropic instances and DSH (whose grant never refreshes) also skip a needs-reauth row.
|
|
503
503
|
if (row.paused) throw new Error("account is paused; quota probe skipped");
|
|
504
|
-
if (row.needsReauth && isAnthropicInstanceId(provider)) throw new Error("account needs sign-in; quota probe skipped");
|
|
504
|
+
if (row.needsReauth && (isAnthropicInstanceId(provider) || provider === "dsh-account")) throw new Error("account needs sign-in; quota probe skipped");
|
|
505
505
|
const stored = row.credential;
|
|
506
506
|
if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
|
|
507
507
|
const activeId = getAccountSet(provider)?.activeAccountId;
|
|
@@ -519,7 +519,8 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
|
|
|
519
519
|
export function accountQuotaProbeSkip(provider: string, accountId: string): AccountQuotaCacheEntry | undefined {
|
|
520
520
|
if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
|
|
521
521
|
if (getAccountCredentialWithStatus(provider, accountId)?.paused !== true) return undefined;
|
|
522
|
-
|
|
522
|
+
const cached = accountQuotaCache.get(accountCacheKey(provider, accountId));
|
|
523
|
+
return cached && cached.isCurrent?.() !== false ? cached : { ts: Date.now(), quota: null, unavailable: true };
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
export function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
|
|
@@ -552,5 +553,5 @@ export function explicitQuotaDestination(provider: string, config: OcxProviderCo
|
|
|
552
553
|
if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
|
|
553
554
|
// These readers use fixed canonical billing origins, never config.baseUrl. Devin reads
|
|
554
555
|
// the credential's own allowlisted api-server host instead (fetchDevinQuota revalidates it).
|
|
555
|
-
return provider === "xai" || provider === "cursor" || provider === "devin";
|
|
556
|
+
return provider === "xai" || provider === "cursor" || provider === "devin" || provider === "dsh-account";
|
|
556
557
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getAccountCredential } from "../../oauth/store";
|
|
2
|
+
import type { OcxProviderConfig } from "../../types";
|
|
3
|
+
import { isAnthropicInstanceId } from "../anthropic-instance-id";
|
|
4
|
+
import {
|
|
5
|
+
explicitAccountReader, explicitQuotaIdentity, normalizeAnthropicQuota,
|
|
6
|
+
type AccountQuotaCacheEntry, type ProviderAccountQuota,
|
|
7
|
+
} from "./account-cache";
|
|
8
|
+
|
|
9
|
+
/** Preserve private ownership fences until management's final publication check. */
|
|
10
|
+
export function projectAccountQuotaRow(
|
|
11
|
+
provider: string,
|
|
12
|
+
accountId: string,
|
|
13
|
+
entry: AccountQuotaCacheEntry,
|
|
14
|
+
providerConfig?: OcxProviderConfig,
|
|
15
|
+
): ProviderAccountQuota {
|
|
16
|
+
const result: ProviderAccountQuota = {
|
|
17
|
+
accountId,
|
|
18
|
+
quota: isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
|
|
19
|
+
...(entry.unavailable ? { unavailable: true as const } : {}),
|
|
20
|
+
...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
|
|
21
|
+
};
|
|
22
|
+
if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
|
|
23
|
+
if (explicitAccountReader(provider)) {
|
|
24
|
+
const identity = entry.identity;
|
|
25
|
+
Object.defineProperty(result, "isCurrent", { value: () => {
|
|
26
|
+
if (entry.isCurrent) return entry.isCurrent();
|
|
27
|
+
const credential = getAccountCredential(provider, accountId);
|
|
28
|
+
return !!credential && (!identity || explicitQuotaIdentity(provider, accountId, providerConfig) === identity);
|
|
29
|
+
} });
|
|
30
|
+
} else if (entry.isCurrent) {
|
|
31
|
+
Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
@@ -71,19 +71,40 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
|
|
|
71
71
|
if (!models) return [];
|
|
72
72
|
|
|
73
73
|
const windows = new Map<string, ProviderQuotaWindow>();
|
|
74
|
+
const modelWindows: ProviderQuotaWindow[] = [];
|
|
75
|
+
const labels = new Set(["Gem", "Cla"]);
|
|
74
76
|
for (const [modelId, rawModelInfo] of Object.entries(models)) {
|
|
75
77
|
const modelInfo = asRecord(rawModelInfo);
|
|
76
78
|
if (!modelInfo) continue;
|
|
77
79
|
for (const quotaInfo of quotaInfoEntries(modelInfo)) {
|
|
78
80
|
const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
|
|
79
|
-
if (!label || windows.has(label)) continue;
|
|
80
81
|
const percent = antigravityUsedPercent(quotaInfo);
|
|
81
82
|
if (percent === undefined) continue;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
percent,
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
const resetAt = normalizeResetAt(quotaInfo.resetTime);
|
|
84
|
+
if (label && !windows.has(label)) {
|
|
85
|
+
windows.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
|
|
86
|
+
}
|
|
87
|
+
// Keep model measurements as display/diagnostic evidence. They are deliberately
|
|
88
|
+
// separate from the family summaries above; routing must not infer eligibility
|
|
89
|
+
// from one model's counter or from a missing row.
|
|
90
|
+
if (modelId.trim() && modelId.length <= 128 && modelWindows.length < 256) {
|
|
91
|
+
const tier = typeof quotaInfo.tier === "string" && quotaInfo.tier.length <= 64
|
|
92
|
+
? quotaInfo.tier.trim().slice(0, Math.max(0, 125 - modelId.length))
|
|
93
|
+
: "";
|
|
94
|
+
const baseLabel = `${modelId}${tier ? ` · ${tier}` : ""}`;
|
|
95
|
+
let modelLabel = baseLabel;
|
|
96
|
+
for (let ordinal = 2; labels.has(modelLabel); ordinal += 1) {
|
|
97
|
+
const suffix = ` #${ordinal}`;
|
|
98
|
+
modelLabel = `${baseLabel.slice(0, 128 - suffix.length)}${suffix}`;
|
|
99
|
+
}
|
|
100
|
+
labels.add(modelLabel);
|
|
101
|
+
modelWindows.push({
|
|
102
|
+
label: modelLabel,
|
|
103
|
+
modelId,
|
|
104
|
+
percent,
|
|
105
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
|
|
@@ -91,7 +112,7 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
|
|
|
91
112
|
const window = windows.get(label);
|
|
92
113
|
return window ? [window] : [];
|
|
93
114
|
});
|
|
94
|
-
return customWindows;
|
|
115
|
+
return [...customWindows, ...modelWindows];
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DSH_PLATFORM_ORIGIN } from "../../oauth/dsh";
|
|
2
|
+
import { asRecord, QUOTA_JSON_READ_FAILURE, readQuotaJson, REQUEST_TIMEOUT_MS } from "../quota-wire";
|
|
3
|
+
import { AUTHORITATIVE_EMPTY_QUOTA, TERMINAL_QUOTA_FAILURE, type ProviderQuotaProbeResult } from "./report-cache";
|
|
4
|
+
import { credentialGeneration, markAccountNeedsReauthIfGeneration } from "../../oauth/store";
|
|
5
|
+
import type { OAuthCredentials } from "../../oauth/types";
|
|
6
|
+
|
|
7
|
+
/** The stored account whose token is probed; a rejection retires exactly this credential generation. */
|
|
8
|
+
export interface DshQuotaProbeOwner {
|
|
9
|
+
accountId: string;
|
|
10
|
+
credential: OAuthCredentials;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function fetchDshAccountQuota(
|
|
14
|
+
provider: string,
|
|
15
|
+
accessToken: string,
|
|
16
|
+
owner?: DshQuotaProbeOwner,
|
|
17
|
+
): Promise<ProviderQuotaProbeResult> {
|
|
18
|
+
if (!accessToken || !accessToken.trim()) return null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`${DSH_PLATFORM_ORIGIN}/api/v0/users/get_user_summary`, {
|
|
22
|
+
method: "GET",
|
|
23
|
+
headers: {
|
|
24
|
+
"x-dsh-auth-token": accessToken.trim(),
|
|
25
|
+
"Accept": "application/json",
|
|
26
|
+
},
|
|
27
|
+
redirect: "manual",
|
|
28
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (response.status === 401) {
|
|
32
|
+
// A DSH grant never refreshes, so the platform rejecting it retires this credential the same
|
|
33
|
+
// way an inference 401 does. Generation-checked: a re-import that landed meanwhile stays usable.
|
|
34
|
+
if (owner) {
|
|
35
|
+
await markAccountNeedsReauthIfGeneration(provider, owner.accountId, credentialGeneration(owner.credential))
|
|
36
|
+
.catch(() => false);
|
|
37
|
+
}
|
|
38
|
+
return TERMINAL_QUOTA_FAILURE;
|
|
39
|
+
}
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bodyRaw = await readQuotaJson(response);
|
|
45
|
+
if (bodyRaw === QUOTA_JSON_READ_FAILURE) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = asRecord(bodyRaw);
|
|
50
|
+
if (!body || body.code !== 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = asRecord(body.data);
|
|
55
|
+
if (data?.biz_code !== 0) return null;
|
|
56
|
+
const bizData = asRecord(data.biz_data);
|
|
57
|
+
const summary = asRecord(bizData?.user_summary) ?? bizData;
|
|
58
|
+
if (!summary) return null;
|
|
59
|
+
|
|
60
|
+
// DeepSeek platform reports monetary wallet amounts (normal_wallets, bonus_wallets, total_costs)
|
|
61
|
+
// without capacity or percentage-based rate-limit windows.
|
|
62
|
+
// Fabricating a synthetic percent: 0 custom window would falsely mark an empty wallet (¥0.00)
|
|
63
|
+
// as "0% used" (healthy quota) and pollute quota exhaustion / account ranking.
|
|
64
|
+
// Instead, return AUTHORITATIVE_EMPTY_QUOTA to confirm the credential is active while leaving
|
|
65
|
+
// quota routing evidence unknown.
|
|
66
|
+
return AUTHORITATIVE_EMPTY_QUOTA;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|