@bitkyc08/opencodex 2.86.0 → 2.87.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-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
- 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.ts +35 -8
- 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 +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -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 +11 -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 +38 -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 +17 -14
- 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/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 +12 -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/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 +52 -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 +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- 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/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 +33 -7
- 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 +5 -4
- 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/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 +24 -2
- 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/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 +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -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 +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- 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 +101 -21
- 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 +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -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 +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- 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/request-prepare.ts +76 -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 +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- 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 +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -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 +22 -9
- 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/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- 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
|
@@ -8,7 +8,7 @@ import { captureOAuthAccountSelection, credentialGeneration, getAccountCredentia
|
|
|
8
8
|
import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
|
|
9
9
|
import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
|
|
10
10
|
import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
|
|
11
|
-
import {
|
|
11
|
+
import { readAnthropicUsageQuota } from "../claude-cli-usage";
|
|
12
12
|
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "../xai-transport";
|
|
13
13
|
import {
|
|
14
14
|
commitKiroAccountUsageState,
|
|
@@ -23,7 +23,6 @@ import { providerCodexAccountMode } from "../registry";
|
|
|
23
23
|
import {
|
|
24
24
|
accountReportCurrent,
|
|
25
25
|
TERMINAL_QUOTA_FAILURE,
|
|
26
|
-
hasQuotaRows,
|
|
27
26
|
providerLabel,
|
|
28
27
|
providerQuotaFromCodexQuota,
|
|
29
28
|
publicCapacityAggregation,
|
|
@@ -228,99 +227,8 @@ export async function fetchXaiQuota(provider: string, context: { accessToken: st
|
|
|
228
227
|
}
|
|
229
228
|
}
|
|
230
229
|
|
|
231
|
-
function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
|
|
232
|
-
const rec = asRecord(value);
|
|
233
|
-
if (!rec) return null;
|
|
234
|
-
const percent = normalizePercent(rec.utilization);
|
|
235
|
-
const resetAt = normalizeResetAt(rec.resets_at);
|
|
236
|
-
if (percent === undefined && resetAt === undefined) return null;
|
|
237
|
-
return { percent, resetAt };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
|
|
241
|
-
|
|
242
|
-
function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
|
|
243
|
-
const rec = asRecord(value);
|
|
244
|
-
if (!rec) return null;
|
|
245
|
-
const percent = normalizePercent(rec.percent);
|
|
246
|
-
if (percent === undefined) return null;
|
|
247
|
-
const scope = asRecord(rec.scope);
|
|
248
|
-
const model = asRecord(scope?.model);
|
|
249
|
-
const rawLabel = String(model?.display_name ?? "")
|
|
250
|
-
.replace(TERMINAL_CONTROL_CHARACTERS, "")
|
|
251
|
-
.trim();
|
|
252
|
-
if (!rawLabel) return null;
|
|
253
|
-
const lowerLabel = rawLabel.toLowerCase();
|
|
254
|
-
const label = lowerLabel.includes("fable") ? "Fable"
|
|
255
|
-
: lowerLabel.includes("opus") ? "Opus"
|
|
256
|
-
: lowerLabel.includes("sonnet") ? "Sonnet"
|
|
257
|
-
: null;
|
|
258
|
-
// An unrecognized display_name is never published as a quota label: stripping
|
|
259
|
-
// control characters still leaves attacker-chosen residue on the quota line.
|
|
260
|
-
if (label === null) return null;
|
|
261
|
-
const resetAt = normalizeResetAt(rec.resets_at);
|
|
262
|
-
// Model scope is proven structurally here, not guessed from text: the caller admits only
|
|
263
|
-
// `kind: "weekly_scoped"`, and a limit without a recognized `scope.model.display_name` has
|
|
264
|
-
// already returned null above. Routing keys on `scope`, never on `label`.
|
|
265
|
-
return { label, scope: "model", percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
269
230
|
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
270
231
|
|
|
271
|
-
async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
|
|
272
|
-
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
273
|
-
headers: {
|
|
274
|
-
Accept: "application/json, text/plain, */*",
|
|
275
|
-
"Content-Type": "application/json",
|
|
276
|
-
"User-Agent": CLAUDE_CLI_USER_AGENT,
|
|
277
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
|
278
|
-
Authorization: `Bearer ${accessToken}`,
|
|
279
|
-
},
|
|
280
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
281
|
-
});
|
|
282
|
-
if (!response.ok) return null;
|
|
283
|
-
const body = asRecord(await readQuotaJson(response));
|
|
284
|
-
if (!body) return null;
|
|
285
|
-
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
286
|
-
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
287
|
-
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
288
|
-
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
289
|
-
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
290
|
-
const customWindows: ProviderQuotaWindow[] = [];
|
|
291
|
-
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
292
|
-
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
293
|
-
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
294
|
-
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
295
|
-
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
296
|
-
for (const rawLimit of limits) {
|
|
297
|
-
const limitRecord = asRecord(rawLimit);
|
|
298
|
-
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
299
|
-
// buckets above; only model-scoped weekly limits add a third window.
|
|
300
|
-
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
301
|
-
const limit = parseClaudeLimit(rawLimit);
|
|
302
|
-
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
303
|
-
knownLabels.add(limit.label.toLowerCase());
|
|
304
|
-
customWindows.push(limit);
|
|
305
|
-
}
|
|
306
|
-
const quota: ProviderQuota = {
|
|
307
|
-
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
308
|
-
// rows: report it in the canonical fields so the dashboard renders it with the standard
|
|
309
|
-
// "5-hour limit" label and ordering instead of as a generic extra window.
|
|
310
|
-
...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
|
|
311
|
-
...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
312
|
-
...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
|
|
313
|
-
...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
|
|
314
|
-
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
315
|
-
updatedAt: Date.now(),
|
|
316
|
-
};
|
|
317
|
-
// Empty / schema-changed payloads must not cache as "success with no bars".
|
|
318
|
-
return hasQuotaRows(quota) ? markAnthropicFamilyEnumeration(quota, Array.isArray(body.limits) && body.limits.every(raw => {
|
|
319
|
-
const kind = asRecord(raw)?.kind;
|
|
320
|
-
return kind === "session" || kind === "weekly_all" || kind === "weekly_scoped" && parseClaudeLimit(raw) !== null;
|
|
321
|
-
})) : null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
232
|
/**
|
|
325
233
|
* Anthropic per-credential usage.
|
|
326
234
|
*
|
|
@@ -19,6 +19,8 @@ export interface ProviderQuotaWindow {
|
|
|
19
19
|
label: string;
|
|
20
20
|
percent: number;
|
|
21
21
|
resetAt?: number;
|
|
22
|
+
/** Exact CCA wire model measured by this window; display evidence only, not eligibility. */
|
|
23
|
+
modelId?: string;
|
|
22
24
|
/**
|
|
23
25
|
* Set only when the PRODUCER proved this window covers one model family, structurally rather
|
|
24
26
|
* than by reading its label: an Anthropic `seven_day_<family>` body key, or a limit with
|
package/src/providers/quota.ts
CHANGED
|
@@ -53,7 +53,6 @@ import {
|
|
|
53
53
|
hydrateAccountQuotaCache,
|
|
54
54
|
mayCommitAccountQuotaKey,
|
|
55
55
|
mayCommitProviderQuotaKey,
|
|
56
|
-
normalizeAnthropicQuota,
|
|
57
56
|
supportsPerAccountQuota,
|
|
58
57
|
type AccountQuotaCacheEntry,
|
|
59
58
|
type ProviderAccountQuota,
|
|
@@ -367,6 +366,7 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con
|
|
|
367
366
|
case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
|
|
368
367
|
case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
|
|
369
368
|
case "devin": result = await (await import("./quota/devin")).fetchDevinQuota(provider, accessToken, credential.apiBaseUrl, config.baseUrl); break;
|
|
369
|
+
case "dsh-account": result = await (await import("./quota/dsh-account")).fetchDshAccountQuota(provider, accessToken, { accountId, credential }); break;
|
|
370
370
|
default: return null;
|
|
371
371
|
}
|
|
372
372
|
return { result, identity, isCurrent };
|
|
@@ -424,7 +424,8 @@ async function fetchAccountQuota(
|
|
|
424
424
|
providerConfig?: OcxProviderConfig,
|
|
425
425
|
liveConfig?: OcxConfig,
|
|
426
426
|
): Promise<AccountQuotaCacheEntry> {
|
|
427
|
-
|
|
427
|
+
const skipped = accountQuotaProbeSkip(provider, accountId);
|
|
428
|
+
if (skipped) return skipped;
|
|
428
429
|
if (isAnthropicInstanceId(provider)) return fetchAnthropicAccountQuota(provider, accountId, forceRefresh, liveConfig);
|
|
429
430
|
if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
|
|
430
431
|
if (provider === "kiro") hydrateAccountQuotaCache();
|
|
@@ -432,7 +433,8 @@ async function fetchAccountQuota(
|
|
|
432
433
|
const writerGeneration = captureConfigGeneration();
|
|
433
434
|
const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
|
|
434
435
|
const cachedCandidate = accountQuotaCache.get(key);
|
|
435
|
-
const cached = provider !== "kiro" || cachedCandidate?.identity === kiroIdentity
|
|
436
|
+
const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
|
|
437
|
+
&& cachedCandidate?.isCurrent?.() !== false ? cachedCandidate : undefined;
|
|
436
438
|
if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
|
|
437
439
|
if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
|
|
438
440
|
return cached;
|
|
@@ -454,6 +456,7 @@ async function fetchAccountQuota(
|
|
|
454
456
|
try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
|
|
455
457
|
catch { return false; }
|
|
456
458
|
};
|
|
459
|
+
const antigravityIsCurrent = () => quotaFailureIsCurrent();
|
|
457
460
|
const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
|
|
458
461
|
const unavailable = (): AccountQuotaCacheEntry => {
|
|
459
462
|
const entry: AccountQuotaCacheEntry = {
|
|
@@ -461,9 +464,11 @@ async function fetchAccountQuota(
|
|
|
461
464
|
quota: cached?.quota ?? null,
|
|
462
465
|
unavailable: true,
|
|
463
466
|
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
467
|
+
...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
|
|
464
468
|
...diagnosticFields(),
|
|
465
469
|
};
|
|
466
|
-
if (
|
|
470
|
+
if (entry.isCurrent?.() !== false && mayCommitAccountQuotaKey(key, writerGeneration)
|
|
471
|
+
&& (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
467
472
|
accountQuotaCache.set(key, entry);
|
|
468
473
|
if (provider === "kiro") persistKiroAccountState();
|
|
469
474
|
sweepExpiredOnWrite(entry.ts);
|
|
@@ -493,12 +498,15 @@ async function fetchAccountQuota(
|
|
|
493
498
|
return { ts: Date.now(), quota: null, unavailable: true };
|
|
494
499
|
}
|
|
495
500
|
}
|
|
501
|
+
if (provider === "google-antigravity" && !antigravityIsCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
|
|
496
502
|
if (!quota) return unavailable();
|
|
497
503
|
const entry: AccountQuotaCacheEntry = {
|
|
498
504
|
ts: Date.now(), quota,
|
|
499
505
|
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
506
|
+
...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
|
|
500
507
|
};
|
|
501
508
|
if (mayCommitAccountQuotaKey(key, writerGeneration)
|
|
509
|
+
&& entry.isCurrent?.() !== false
|
|
502
510
|
&& (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
503
511
|
accountQuotaCache.set(key, entry);
|
|
504
512
|
if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); }
|
|
@@ -530,21 +538,7 @@ export async function fetchProviderAccountQuotas(
|
|
|
530
538
|
if (!set) return [];
|
|
531
539
|
return mapQuotaRoster(set.accounts, async account => {
|
|
532
540
|
const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig, liveConfig);
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
quota: isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
|
|
536
|
-
...(entry.unavailable ? { unavailable: true as const } : {}),
|
|
537
|
-
...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
|
|
538
|
-
};
|
|
539
|
-
if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
|
|
540
|
-
if (isAnthropicInstanceId(provider) && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
|
|
541
|
-
if (!explicitAccountReader(provider)) return result;
|
|
542
|
-
const identity = entry.identity;
|
|
543
|
-
Object.defineProperty(result, "isCurrent", { value: () => {
|
|
544
|
-
if (entry.isCurrent) return entry.isCurrent();
|
|
545
|
-
const credential = getAccountCredential(provider, account.id);
|
|
546
|
-
return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
|
|
547
|
-
} });
|
|
548
|
-
return result;
|
|
541
|
+
const { projectAccountQuotaRow } = await import("./quota/account-row");
|
|
542
|
+
return projectAccountQuotaRow(provider, account.id, entry, providerConfig);
|
|
549
543
|
});
|
|
550
544
|
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { OPENCODE_GO_BASE_URL, OPENCODE_GO_RESPONSES_DEFAULTS } from "../../types/opencode-go";
|
|
1
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "../kiro-models";
|
|
3
|
+
import {
|
|
4
|
+
DSH_ACCOUNT_MODELS,
|
|
5
|
+
DSH_ACCOUNT_DEFAULT_MODEL,
|
|
6
|
+
DSH_ACCOUNT_MODEL_CONTEXT_WINDOWS,
|
|
7
|
+
DSH_ACCOUNT_MODEL_INPUT_MODALITIES,
|
|
8
|
+
DSH_ACCOUNT_MODEL_MAX_OUTPUT_TOKENS,
|
|
9
|
+
DSH_ACCOUNT_MODEL_REASONING_EFFORTS,
|
|
10
|
+
DSH_ACCOUNT_MODEL_DISPLAY_NAMES,
|
|
11
|
+
} from "../dsh-account-models";
|
|
2
12
|
import { DEVIN_MODEL_CONTEXT_WINDOWS, DEVIN_MODEL_EFFORTS, DEVIN_DEFAULT_EFFORTS } from "../../adapters/devin/live-models";
|
|
3
13
|
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "../antigravity-models";
|
|
4
14
|
import {
|
|
@@ -506,6 +516,27 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
506
516
|
// The proprietary generate wire has no verified per-request serialization flag.
|
|
507
517
|
parallelToolCalls: false,
|
|
508
518
|
},
|
|
519
|
+
{
|
|
520
|
+
id: "dsh-account",
|
|
521
|
+
label: "DeepSeek Account (DSH)",
|
|
522
|
+
adapter: "dsh-account",
|
|
523
|
+
baseUrl: "https://api.deepseek.com/anthropic",
|
|
524
|
+
authKind: "oauth",
|
|
525
|
+
oauthId: "dsh-account",
|
|
526
|
+
featured: false,
|
|
527
|
+
dashboardPreset: true,
|
|
528
|
+
note: "Import-first: reuses your installed and signed-in DeepSeek Harness Desktop account. Add account imports the grant from ~/.dsh/.credentials.yaml and verifies it with the DeepSeek platform.",
|
|
529
|
+
dashboardUrl: "https://platform.deepseek.com",
|
|
530
|
+
// Static catalog: the account token never authorizes model discovery (src/oauth/model-discovery-auth.ts).
|
|
531
|
+
liveModels: false,
|
|
532
|
+
defaultModel: DSH_ACCOUNT_DEFAULT_MODEL,
|
|
533
|
+
models: [...DSH_ACCOUNT_MODELS],
|
|
534
|
+
modelContextWindows: DSH_ACCOUNT_MODEL_CONTEXT_WINDOWS,
|
|
535
|
+
modelInputModalities: DSH_ACCOUNT_MODEL_INPUT_MODALITIES,
|
|
536
|
+
modelMaxOutputTokens: DSH_ACCOUNT_MODEL_MAX_OUTPUT_TOKENS,
|
|
537
|
+
modelReasoningEfforts: DSH_ACCOUNT_MODEL_REASONING_EFFORTS,
|
|
538
|
+
modelDisplayNames: DSH_ACCOUNT_MODEL_DISPLAY_NAMES,
|
|
539
|
+
},
|
|
509
540
|
{
|
|
510
541
|
id: "orcarouter-oauth",
|
|
511
542
|
label: "OrcaRouter - Auth",
|
|
@@ -824,7 +855,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
824
855
|
escapeBuiltinToolNames: true,
|
|
825
856
|
},
|
|
826
857
|
{
|
|
827
|
-
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl:
|
|
858
|
+
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: OPENCODE_GO_BASE_URL,
|
|
859
|
+
destinationAliases: [{ adapter: "openai-chat", baseUrl: "https://opencode.ai/inference/go/v1" }],
|
|
828
860
|
authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
|
|
829
861
|
jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
|
|
830
862
|
// Zen Go can close a Chat stream after a fully assembled function call without sending
|
|
@@ -846,13 +878,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
846
878
|
- 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence.
|
|
847
879
|
- 장점, 단점 및 영향: Each listed model reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
|
|
848
880
|
*/
|
|
849
|
-
modelWireDefaults:
|
|
850
|
-
"gpt-5.6-luna": "openai-responses",
|
|
851
|
-
"grok-4.6": "openai-responses",
|
|
852
|
-
"grok-4.7": "openai-responses",
|
|
853
|
-
"muse-spark-1.3-contributor": "openai-responses",
|
|
854
|
-
"muse-spark-1.2-contributor": "openai-responses",
|
|
855
|
-
},
|
|
881
|
+
modelWireDefaults: OPENCODE_GO_RESPONSES_DEFAULTS,
|
|
856
882
|
modelContextWindows: {
|
|
857
883
|
"kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
|
|
858
884
|
// Zen Go discovers only the gateway id, so carry DeepSeek's official 1M V4.1
|
|
@@ -909,6 +935,9 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
909
935
|
},
|
|
910
936
|
modelReasoningEfforts: {
|
|
911
937
|
"gpt-5.6-luna": OPENAI_API_GPT56_REASONING_EFFORTS,
|
|
938
|
+
// Gemini's native thinkingLevel ladder; higher Codex efforts clamp to high.
|
|
939
|
+
// https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
|
|
940
|
+
"gemini-4-argon-contributor": ["low", "medium", "high"],
|
|
912
941
|
"grok-4.6": ["low", "medium", "high", "xhigh"],
|
|
913
942
|
"grok-4.7": ["low", "medium", "high", "xhigh"],
|
|
914
943
|
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
|
|
@@ -213,7 +213,7 @@ export interface ProviderRegistryEntry {
|
|
|
213
213
|
*/
|
|
214
214
|
chatCompletionsPath?: string;
|
|
215
215
|
/**
|
|
216
|
-
*
|
|
216
|
+
* Canonical or historical endpoints, kept so a saved custom provider that still points
|
|
217
217
|
* at one keeps receiving this row's metadata through `registryEntryForProviderDestination`.
|
|
218
218
|
* Destination matching is by adapter plus normalized base URL, so moving a row's wire or
|
|
219
219
|
* prefix would otherwise orphan every config a user wrote against the old address.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxProviderConfig, RequestPacingRule } from "../types";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import type { GenerationContext } from "../lib/state-store-sweeper";
|
|
3
4
|
import { carryReplayRefusal, isNonReplayableResponse, markResponseNonReplayable } from "../lib/upstream-retry";
|
|
4
5
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -39,7 +40,7 @@ export function registerProviderResponseProvenance(markers: WeakMap<Response, un
|
|
|
39
40
|
responseProvenance.add(markers);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired";
|
|
43
|
+
export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired" | "keys_exhausted";
|
|
43
44
|
|
|
44
45
|
export class RequestPacingQueueOverloadError extends Error {
|
|
45
46
|
constructor(
|
|
@@ -47,7 +48,7 @@ export class RequestPacingQueueOverloadError extends Error {
|
|
|
47
48
|
public readonly reason: RequestPacingQueueOverloadReason,
|
|
48
49
|
public readonly retryAfterSeconds: number,
|
|
49
50
|
) {
|
|
50
|
-
super(reason === "queue_full"
|
|
51
|
+
super(reason === "keys_exhausted" ? "No eligible API key remains; wait for the provider cooldown before retrying" : reason === "queue_full"
|
|
51
52
|
? `request pacing queue for provider '${providerName}' is full`
|
|
52
53
|
: `request pacing queue for provider '${providerName}' exceeded the maximum queued age`);
|
|
53
54
|
this.name = "RequestPacingQueueOverloadError";
|
|
@@ -62,6 +63,10 @@ export class RequestPacingProviderRemovedError extends Error {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
interface Waiter {
|
|
66
|
+
keyLease: boolean;
|
|
67
|
+
keyId?: string;
|
|
68
|
+
keyIntervalMs: number;
|
|
69
|
+
keyMaxConcurrent: number;
|
|
65
70
|
modelId?: string;
|
|
66
71
|
providerIntervalMs: number;
|
|
67
72
|
modelIntervalMs: number;
|
|
@@ -75,6 +80,9 @@ interface Waiter {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
interface ProviderPacer {
|
|
83
|
+
keyNextStartAt: Map<string, number>;
|
|
84
|
+
keyInFlight: Map<string, number>;
|
|
85
|
+
keyStarts: Map<string, number>;
|
|
78
86
|
queue: Waiter[];
|
|
79
87
|
providerNextStartAt: number;
|
|
80
88
|
modelNextStartAt: Map<string, number>;
|
|
@@ -138,6 +146,7 @@ export interface ProviderRequestPacingStatus {
|
|
|
138
146
|
inFlight?: number;
|
|
139
147
|
lastStartedAt?: number;
|
|
140
148
|
lastModelId?: string;
|
|
149
|
+
keys?: Array<{ id: string; starts: number; inFlight: number; queued: number; nextSlotInMs: number }>;
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
const pacers = new Map<string, ProviderPacer>();
|
|
@@ -165,6 +174,13 @@ function normalizedInterval(rule: RequestPacingRule | undefined): number {
|
|
|
165
174
|
return Math.max(rpmInterval, fixedInterval);
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
function keyBucket(provider: OcxProviderConfig): string | undefined {
|
|
178
|
+
if (provider.authMode === "oauth" || provider.authMode === "forward" || !provider.apiKey) return;
|
|
179
|
+
if (!provider.requestPacing?.enabled || (!provider.requestPacing.keys && provider.requestPacing.keySelection !== "round-robin")) return;
|
|
180
|
+
// Internal only: duplicate references share one bucket; never serialize a key or digest.
|
|
181
|
+
return createHash("sha256").update(provider._apiKeyAttempt?.reference ?? provider.apiKey).digest("hex");
|
|
182
|
+
}
|
|
183
|
+
|
|
168
184
|
function normalizedMaxConcurrent(rule: RequestPacingRule | undefined): number {
|
|
169
185
|
return typeof rule?.maxConcurrentRequests === "number" && rule.maxConcurrentRequests > 0
|
|
170
186
|
// Floor so runtime-injected configs that bypass the integer schema cannot admit
|
|
@@ -178,10 +194,14 @@ export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: s
|
|
|
178
194
|
const policy = provider.requestPacing;
|
|
179
195
|
if (!policy?.enabled) return 0;
|
|
180
196
|
const override = modelId ? policy.models?.[modelId] : undefined;
|
|
181
|
-
return Math.max(normalizedInterval(policy), normalizedInterval(override));
|
|
197
|
+
return Math.max(normalizedInterval(policy), normalizedInterval(override), keyBucket(provider) ? normalizedInterval(policy.keys) : 0);
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string): {
|
|
201
|
+
keyLease: boolean;
|
|
202
|
+
keyId?: string;
|
|
203
|
+
keyIntervalMs: number;
|
|
204
|
+
keyMaxConcurrent: number;
|
|
185
205
|
providerIntervalMs: number;
|
|
186
206
|
modelIntervalMs: number;
|
|
187
207
|
providerMaxConcurrent: number;
|
|
@@ -189,9 +209,14 @@ function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string):
|
|
|
189
209
|
} {
|
|
190
210
|
const policy = provider.requestPacing;
|
|
191
211
|
if (!policy?.enabled) {
|
|
192
|
-
return { providerIntervalMs: 0, modelIntervalMs: 0, providerMaxConcurrent: 0, modelMaxConcurrent: 0 };
|
|
212
|
+
return { keyLease: false, keyIntervalMs: 0, keyMaxConcurrent: 0, providerIntervalMs: 0, modelIntervalMs: 0, providerMaxConcurrent: 0, modelMaxConcurrent: 0 };
|
|
193
213
|
}
|
|
214
|
+
const keyId = keyBucket(provider);
|
|
194
215
|
return {
|
|
216
|
+
keyLease: true,
|
|
217
|
+
keyId,
|
|
218
|
+
keyIntervalMs: keyId ? normalizedInterval(policy.keys) : 0,
|
|
219
|
+
keyMaxConcurrent: keyId ? normalizedMaxConcurrent(policy.keys) : 0,
|
|
195
220
|
providerIntervalMs: normalizedInterval(policy),
|
|
196
221
|
modelIntervalMs: modelId ? normalizedInterval(policy.models?.[modelId]) : 0,
|
|
197
222
|
providerMaxConcurrent: normalizedMaxConcurrent(policy),
|
|
@@ -209,13 +234,14 @@ function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string):
|
|
|
209
234
|
*/
|
|
210
235
|
export function requestPacingMaxConcurrentRequests(provider: OcxProviderConfig, modelId?: string): number {
|
|
211
236
|
const limits = requestPacingIntervals(provider, modelId);
|
|
212
|
-
return Math.max(limits.providerMaxConcurrent, limits.modelMaxConcurrent);
|
|
237
|
+
return Math.max(limits.providerMaxConcurrent, limits.modelMaxConcurrent, limits.keyMaxConcurrent);
|
|
213
238
|
}
|
|
214
239
|
|
|
215
|
-
function waiterReadyAt(state: ProviderPacer, modelId: string | undefined): number {
|
|
240
|
+
function waiterReadyAt(state: ProviderPacer, modelId: string | undefined, keyId?: string): number {
|
|
216
241
|
return Math.max(
|
|
217
242
|
state.providerNextStartAt,
|
|
218
243
|
modelId ? (state.modelNextStartAt.get(modelId) ?? 0) : 0,
|
|
244
|
+
keyId ? (state.keyNextStartAt.get(keyId) ?? 0) : 0,
|
|
219
245
|
);
|
|
220
246
|
}
|
|
221
247
|
|
|
@@ -224,8 +250,9 @@ function pacingRetryAfterSeconds(
|
|
|
224
250
|
modelId: string | undefined,
|
|
225
251
|
now: number,
|
|
226
252
|
concurrencyCapped = false,
|
|
253
|
+
keyId?: string,
|
|
227
254
|
): number {
|
|
228
|
-
const intervalSeconds = Math.max(1, Math.ceil(Math.max(0, waiterReadyAt(state, modelId) - now) / 1000));
|
|
255
|
+
const intervalSeconds = Math.max(1, Math.ceil(Math.max(0, waiterReadyAt(state, modelId, keyId) - now) / 1000));
|
|
229
256
|
// A waiter blocked by an in-flight lease has no interval to report: when the wait ends
|
|
230
257
|
// in refusal, the honest answer is that the cap is saturated, not "retry every second".
|
|
231
258
|
return concurrencyCapped
|
|
@@ -240,7 +267,7 @@ function makeProviderRequestSlot(
|
|
|
240
267
|
): BodyTrackableProviderRequestSlot {
|
|
241
268
|
let released = false;
|
|
242
269
|
let bodyTracked = false;
|
|
243
|
-
const leased = waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0;
|
|
270
|
+
const leased = (!!waiter.keyId && waiter.keyLease) || waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0;
|
|
244
271
|
const slot: BodyTrackableProviderRequestSlot = {
|
|
245
272
|
providerName,
|
|
246
273
|
leased,
|
|
@@ -258,6 +285,7 @@ function makeProviderRequestSlot(
|
|
|
258
285
|
released = true;
|
|
259
286
|
waiter.signal?.removeEventListener("abort", releaseOnAbort);
|
|
260
287
|
if (waiter.providerMaxConcurrent > 0 && state.providerInFlight > 0) state.providerInFlight -= 1;
|
|
288
|
+
if (waiter.keyId && waiter.keyLease) state.keyInFlight.set(waiter.keyId, Math.max(0, (state.keyInFlight.get(waiter.keyId) ?? 0) - 1));
|
|
261
289
|
if (waiter.modelId && waiter.modelMaxConcurrent > 0) {
|
|
262
290
|
const current = state.modelInFlight.get(waiter.modelId) ?? 0;
|
|
263
291
|
if (current <= 1) state.modelInFlight.delete(waiter.modelId);
|
|
@@ -293,7 +321,8 @@ function rejectExpiredWaiters(providerName: string, state: ProviderPacer, now: n
|
|
|
293
321
|
state,
|
|
294
322
|
waiter.modelId,
|
|
295
323
|
now,
|
|
296
|
-
waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0,
|
|
324
|
+
waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0 || waiter.keyMaxConcurrent > 0,
|
|
325
|
+
waiter.keyId,
|
|
297
326
|
),
|
|
298
327
|
));
|
|
299
328
|
}
|
|
@@ -327,17 +356,18 @@ function runQueue(providerName: string, state: ProviderPacer): void {
|
|
|
327
356
|
|
|
328
357
|
const providerReadyAt = Math.max(now, state.providerNextStartAt);
|
|
329
358
|
const waiterIndex = state.queue.findIndex(waiter => {
|
|
359
|
+
if (waiter.keyId && waiter.keyMaxConcurrent > 0 && (state.keyInFlight.get(waiter.keyId) ?? 0) >= waiter.keyMaxConcurrent) return false;
|
|
330
360
|
if (waiter.providerMaxConcurrent > 0 && state.providerInFlight >= waiter.providerMaxConcurrent) return false;
|
|
331
361
|
if (waiter.modelId && waiter.modelMaxConcurrent > 0
|
|
332
362
|
&& (state.modelInFlight.get(waiter.modelId) ?? 0) >= waiter.modelMaxConcurrent) return false;
|
|
333
363
|
const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
|
|
334
|
-
return Math.max(providerReadyAt, modelReadyAt) <= now;
|
|
364
|
+
return Math.max(providerReadyAt, modelReadyAt, waiter.keyId ? state.keyNextStartAt.get(waiter.keyId) ?? 0 : 0) <= now;
|
|
335
365
|
});
|
|
336
366
|
if (waiterIndex < 0) {
|
|
337
367
|
let earliestAt = Number.POSITIVE_INFINITY;
|
|
338
368
|
for (const waiter of state.queue) {
|
|
339
369
|
const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
|
|
340
|
-
const readyAt = Math.max(providerReadyAt, modelReadyAt);
|
|
370
|
+
const readyAt = Math.max(providerReadyAt, modelReadyAt, waiter.keyId ? state.keyNextStartAt.get(waiter.keyId) ?? 0 : 0);
|
|
341
371
|
const expiresAt = waiter.queuedAt + maxQueueAgeMs;
|
|
342
372
|
// A waiter whose start time already passed is blocked on an in-flight lease; its release
|
|
343
373
|
// re-runs this queue through a microtask, so the timer only needs its expiry backstop.
|
|
@@ -362,6 +392,11 @@ function runQueue(providerName: string, state: ProviderPacer): void {
|
|
|
362
392
|
state.lastStartedAt = startedAt;
|
|
363
393
|
state.lastModelId = waiter.modelId;
|
|
364
394
|
state.providerNextStartAt = startedAt + waiter.providerIntervalMs;
|
|
395
|
+
if (waiter.keyId) {
|
|
396
|
+
state.keyNextStartAt.set(waiter.keyId, startedAt + waiter.keyIntervalMs);
|
|
397
|
+
if (waiter.keyLease) state.keyInFlight.set(waiter.keyId, (state.keyInFlight.get(waiter.keyId) ?? 0) + 1);
|
|
398
|
+
state.keyStarts.set(waiter.keyId, (state.keyStarts.get(waiter.keyId) ?? 0) + 1);
|
|
399
|
+
}
|
|
365
400
|
if (waiter.modelId && waiter.modelIntervalMs > 0) {
|
|
366
401
|
state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs);
|
|
367
402
|
}
|
|
@@ -386,22 +421,34 @@ export async function waitForProviderRequestSlot(
|
|
|
386
421
|
// by interval only, or a follow-up would queue behind the lease its own turn holds.
|
|
387
422
|
const concurrency = options?.concurrency !== false;
|
|
388
423
|
const waiterIntervals = concurrency ? intervals : {
|
|
424
|
+
keyId: intervals.keyId, keyLease: false, keyIntervalMs: intervals.keyIntervalMs, keyMaxConcurrent: 0,
|
|
389
425
|
providerIntervalMs: intervals.providerIntervalMs,
|
|
390
426
|
modelIntervalMs: intervals.modelIntervalMs,
|
|
391
427
|
providerMaxConcurrent: 0,
|
|
392
428
|
modelMaxConcurrent: 0,
|
|
393
429
|
};
|
|
394
430
|
const paced = Math.max(intervals.providerIntervalMs, intervals.modelIntervalMs) > 0
|
|
431
|
+
|| intervals.keyId !== undefined
|
|
395
432
|
|| waiterIntervals.providerMaxConcurrent > 0
|
|
396
433
|
|| waiterIntervals.modelMaxConcurrent > 0;
|
|
397
434
|
if (!paced) return inertProviderRequestSlot;
|
|
398
435
|
if (signal?.aborted) throw abortReason(signal);
|
|
399
436
|
|
|
400
437
|
const state = pacers.get(providerName) ?? {
|
|
438
|
+
keyNextStartAt: new Map<string, number>(), keyInFlight: new Map<string, number>(), keyStarts: new Map<string, number>(),
|
|
401
439
|
queue: [], providerNextStartAt: 0, modelNextStartAt: new Map<string, number>(),
|
|
402
440
|
providerInFlight: 0, modelInFlight: new Map<string, number>(),
|
|
403
441
|
};
|
|
404
442
|
pacers.set(providerName, state);
|
|
443
|
+
// Retire removed credentials only after their queued work, leases and spacing debt end.
|
|
444
|
+
const currentKeys = new Set((provider.apiKeyPool ?? []).map(entry =>
|
|
445
|
+
keyBucket({ ...provider, apiKey: entry.key, _apiKeyAttempt: undefined })));
|
|
446
|
+
currentKeys.add(intervals.keyId);
|
|
447
|
+
for (const key of state.keyStarts.keys()) {
|
|
448
|
+
if (currentKeys.has(key) || (state.keyInFlight.get(key) ?? 0) > 0
|
|
449
|
+
|| (state.keyNextStartAt.get(key) ?? 0) > runtime.now() || state.queue.some(waiter => waiter.keyId === key)) continue;
|
|
450
|
+
state.keyStarts.delete(key); state.keyInFlight.delete(key); state.keyNextStartAt.delete(key);
|
|
451
|
+
}
|
|
405
452
|
|
|
406
453
|
// Give already-eligible or expired waiters a chance to leave before applying the
|
|
407
454
|
// admission bound to the newest request. This preserves FIFO-ish fairness while
|
|
@@ -419,7 +466,8 @@ export async function waitForProviderRequestSlot(
|
|
|
419
466
|
state,
|
|
420
467
|
modelId,
|
|
421
468
|
runtime.now(),
|
|
422
|
-
waiterIntervals.providerMaxConcurrent > 0 || waiterIntervals.modelMaxConcurrent > 0,
|
|
469
|
+
waiterIntervals.providerMaxConcurrent > 0 || waiterIntervals.modelMaxConcurrent > 0 || waiterIntervals.keyMaxConcurrent > 0,
|
|
470
|
+
waiterIntervals.keyId,
|
|
423
471
|
),
|
|
424
472
|
);
|
|
425
473
|
}
|
|
@@ -464,14 +512,13 @@ export async function waitForProviderRequestSlot(
|
|
|
464
512
|
* means the exchange is already finished, so the lease returns immediately. A body that
|
|
465
513
|
* is neither read nor cancelled for REQUEST_PACING_UNCONSUMED_BODY_MS releases its lease
|
|
466
514
|
* and cancels the body, so a dropped Response cannot hold a slot forever. The returned
|
|
467
|
-
* Response preserves status, statusText, headers,
|
|
515
|
+
* Response preserves status, statusText, headers, replay markers, and registered provenance.
|
|
468
516
|
*/
|
|
469
517
|
export function trackProviderRequestSlotBody(
|
|
470
518
|
slot: ProviderRequestSlot | undefined,
|
|
471
519
|
response: Response,
|
|
472
520
|
): Response {
|
|
473
|
-
// An unleased slot has nothing to return on body close
|
|
474
|
-
// would break identity-based markers (the eager WS relay registry is a WeakSet).
|
|
521
|
+
// An unleased slot has nothing to return on body close; preserve the response identity.
|
|
475
522
|
if (!slot?.leased) return response;
|
|
476
523
|
// Slots built outside waitForProviderRequestSlot (test doubles) satisfy only the public
|
|
477
524
|
// ProviderRequestSlot shape; their bodies still own the release through the wrapper below.
|
|
@@ -657,24 +704,35 @@ export function providerRequestPacingStatus(
|
|
|
657
704
|
if (state && state.queue.length > 0) {
|
|
658
705
|
let earliestQueuedSlotAt = Number.POSITIVE_INFINITY;
|
|
659
706
|
for (const waiter of state.queue) {
|
|
660
|
-
earliestQueuedSlotAt = Math.min(earliestQueuedSlotAt, waiterReadyAt(state, waiter.modelId));
|
|
707
|
+
earliestQueuedSlotAt = Math.min(earliestQueuedSlotAt, waiterReadyAt(state, waiter.modelId, waiter.keyId));
|
|
661
708
|
}
|
|
662
709
|
if (Number.isFinite(earliestQueuedSlotAt)) nextSlotAt = earliestQueuedSlotAt;
|
|
663
710
|
}
|
|
664
|
-
const providerConcurrencyCap =
|
|
711
|
+
const providerConcurrencyCap = normalizedMaxConcurrent(provider.requestPacing);
|
|
712
|
+
const hasKeyAccounting = provider.requestPacing?.enabled === true && keyBucket(provider) !== undefined;
|
|
665
713
|
// Gate on enabled so the status surface agrees with enforcement: a disabled requestPacing
|
|
666
714
|
// block must not report in-flight leases alongside enabled: false.
|
|
667
|
-
const anyConcurrencyCap = provider.requestPacing?.enabled === true && (providerConcurrencyCap > 0
|
|
715
|
+
const anyConcurrencyCap = provider.requestPacing?.enabled === true && (hasKeyAccounting || providerConcurrencyCap > 0
|
|
668
716
|
|| Object.values(provider.requestPacing?.models ?? {}).some(rule => normalizedMaxConcurrent(rule) > 0));
|
|
669
717
|
return {
|
|
670
718
|
provider: providerName,
|
|
671
719
|
enabled: provider.requestPacing?.enabled === true,
|
|
672
720
|
queued: state?.queue.length ?? 0,
|
|
673
721
|
nextSlotInMs: Math.max(0, Math.ceil(nextSlotAt - now)),
|
|
722
|
+
...(provider.requestPacing?.enabled && (provider.requestPacing.keys || provider.requestPacing.keySelection === "round-robin") ? {
|
|
723
|
+
keys: (provider.apiKeyPool ?? []).map(entry => {
|
|
724
|
+
const bucket = keyBucket({ ...provider, apiKey: entry.key, _apiKeyAttempt: undefined })!;
|
|
725
|
+
return { id: entry.id, starts: state?.keyStarts.get(bucket) ?? 0, inFlight: state?.keyInFlight.get(bucket) ?? 0,
|
|
726
|
+
queued: state?.queue.filter(waiter => waiter.keyId === bucket).length ?? 0,
|
|
727
|
+
nextSlotInMs: Math.max(0, Math.ceil((state?.keyNextStartAt.get(bucket) ?? 0) - now)) };
|
|
728
|
+
}),
|
|
729
|
+
} : {}),
|
|
674
730
|
// A provider cap counts every paced send in providerInFlight; model-only caps keep
|
|
675
731
|
// providerInFlight at zero, so report the per-model sum for those providers instead.
|
|
676
732
|
...(anyConcurrencyCap ? {
|
|
677
|
-
inFlight:
|
|
733
|
+
inFlight: hasKeyAccounting
|
|
734
|
+
? [...(state?.keyInFlight.values() ?? [])].reduce((sum, count) => sum + count, 0)
|
|
735
|
+
: providerConcurrencyCap > 0
|
|
678
736
|
? state?.providerInFlight ?? 0
|
|
679
737
|
: [...(state?.modelInFlight.values() ?? [])].reduce((sum, count) => sum + count, 0),
|
|
680
738
|
} : {}),
|