@bitkyc08/opencodex 2.80.0 → 2.82.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/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -1,48 +1,15 @@
|
|
|
1
1
|
import { mergeAnthropicFamilyWindows, hasAnthropicFamilyEnumeration } from "./anthropic-family-headers";
|
|
2
|
-
import { getCachedProviderAccountQuota } from "./account-cache";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { getCachedProviderAccountQuota, captureProviderAccountQuotaEpoch } from "./account-cache";
|
|
3
|
+
import { anthropicModelQuotaFor } from "../../oauth/anthropic-model-quota";
|
|
4
|
+
import type { AnthropicInstanceId } from "../anthropic-instance-id";
|
|
5
|
+
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
6
|
+
import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
|
|
5
7
|
import type { ProviderQuota } from "../quota-types";
|
|
6
8
|
|
|
7
9
|
export class AnthropicQuotaProbeOwnershipError extends Error {}
|
|
8
10
|
|
|
9
|
-
/** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
|
|
10
|
-
export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
|
|
11
|
-
const row = getAccountCredentialWithStatus("anthropic", accountId);
|
|
12
|
-
if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
|
|
13
|
-
throw new AnthropicQuotaProbeOwnershipError("anthropic quota account is no longer eligible");
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const cooldownGenerationByAccount = new Map<string, number>();
|
|
18
|
-
let nextCooldownGeneration = 1;
|
|
19
|
-
let afterSettlementForTests: (() => void) | undefined;
|
|
20
|
-
|
|
21
|
-
export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
|
|
22
|
-
afterSettlementForTests = hook;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Monotonic per-account fence shared by routing mutations and quota publication. */
|
|
26
|
-
export function anthropicCooldownGeneration(accountId: string): number {
|
|
27
|
-
return cooldownGenerationByAccount.get(accountId) ?? 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
|
|
31
|
-
export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
|
|
32
|
-
return `${baseKey}\0anthropic-cooldown:${anthropicCooldownGeneration(accountId)}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function noteAnthropicCooldownMutation(accountId: string): number {
|
|
36
|
-
const generation = nextCooldownGeneration++;
|
|
37
|
-
cooldownGenerationByAccount.set(accountId, generation);
|
|
38
|
-
return generation;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function clearAnthropicCooldownGenerations(): void {
|
|
42
|
-
cooldownGenerationByAccount.clear();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
11
|
export type AnthropicCooldownRecoveryProbe = Readonly<{
|
|
12
|
+
instance: AnthropicInstanceId;
|
|
46
13
|
requiresFreshDispatch: boolean;
|
|
47
14
|
isCurrentCredential(): boolean;
|
|
48
15
|
isCurrentFamily(): boolean;
|
|
@@ -51,86 +18,230 @@ export type AnthropicCooldownRecoveryProbe = Readonly<{
|
|
|
51
18
|
}>;
|
|
52
19
|
|
|
53
20
|
export type AnthropicQuotaRecoveryResult = Readonly<{
|
|
21
|
+
instance: AnthropicInstanceId;
|
|
54
22
|
quota: ProviderQuota;
|
|
55
23
|
/** Must be checked synchronously at each publication boundary. */
|
|
56
24
|
isCurrent(): boolean;
|
|
57
25
|
}>;
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* dispatch. A successful response may otherwise arrive after either credential replacement
|
|
62
|
-
* or a newer 429 and incorrectly make unrelated state eligible.
|
|
63
|
-
*/
|
|
64
|
-
export async function captureAnthropicCooldownRecoveryProbe(
|
|
65
|
-
accountId: string,
|
|
66
|
-
accessToken: string,
|
|
67
|
-
): Promise<AnthropicCooldownRecoveryProbe | null> {
|
|
68
|
-
const credential = getAccountCredential("anthropic", accountId);
|
|
69
|
-
if (!credential || credential.access !== accessToken) return null;
|
|
70
|
-
const generation = credentialGeneration(credential);
|
|
71
|
-
// Lazy because anthropic-routing reads the quota cache on normal request routing.
|
|
72
|
-
const routing = await import("../../oauth/anthropic-routing");
|
|
73
|
-
const cooldownGeneration = anthropicCooldownGeneration(accountId);
|
|
74
|
-
const familyGeneration = anthropicFamilyQuotaGeneration(accountId);
|
|
75
|
-
const claim = routing.captureAnthropicCooldownRecovery(accountId);
|
|
76
|
-
const isCurrentCredential = () => {
|
|
77
|
-
const current = getAccountCredential("anthropic", accountId);
|
|
78
|
-
return !!current && credentialGeneration(current) === generation;
|
|
79
|
-
};
|
|
80
|
-
return {
|
|
81
|
-
requiresFreshDispatch: claim !== null,
|
|
82
|
-
isCurrentCredential,
|
|
83
|
-
isCurrentFamily: () => anthropicFamilyQuotaGeneration(accountId) === familyGeneration,
|
|
84
|
-
isCurrent: () => isCurrentCredential()
|
|
85
|
-
&& anthropicCooldownGeneration(accountId) === cooldownGeneration,
|
|
86
|
-
settle: quota => claim === null ? "retained"
|
|
87
|
-
: routing.settleAnthropicCooldownRecovery(claim, quota),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
27
|
+
function createAnthropicCooldownRecovery(instance: AnthropicInstanceId) {
|
|
28
|
+
const familyGeneration = (accountId: string) => anthropicModelQuotaFor(instance).anthropicFamilyQuotaGeneration(accountId);
|
|
90
29
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
30
|
+
/** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
|
|
31
|
+
function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
|
|
32
|
+
const row = getAccountCredentialWithStatus(instance, accountId);
|
|
33
|
+
if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
|
|
34
|
+
throw new AnthropicQuotaProbeOwnershipError(`${instance} quota account is no longer eligible`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let cooldownGenerationByAccount: Map<string, number> | undefined;
|
|
39
|
+
let emptyGeneration = 0;
|
|
40
|
+
let nextCooldownGeneration = 1;
|
|
41
|
+
// Account lifetime is distinct from refusal/probe mutations; a valid refusal must not revoke itself.
|
|
42
|
+
let accountIncarnations: Map<string, number> | undefined;
|
|
43
|
+
let emptyAccountIncarnation = 0;
|
|
44
|
+
let nextAccountIncarnation = 1;
|
|
45
|
+
let afterSettlementForTests: (() => void) | undefined;
|
|
46
|
+
|
|
47
|
+
function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
|
|
48
|
+
afterSettlementForTests = hook;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Monotonic per-account fence shared by routing mutations and quota publication. */
|
|
52
|
+
function anthropicCooldownGeneration(accountId: string): number {
|
|
53
|
+
return cooldownGenerationByAccount?.get(accountId) ?? emptyGeneration;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
|
|
57
|
+
function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
|
|
58
|
+
// Flight reservation establishes an incarnation even without prior refusal evidence.
|
|
59
|
+
const generation = reserveCooldownGeneration(accountId);
|
|
60
|
+
return `${baseKey}\0${instance}-cooldown:${generation}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Active usage readers bind this key after token refresh; renewal keeps the legacy flight valid. */
|
|
64
|
+
function anthropicCredentialQuotaFlightKey(baseKey: string, accountId: string): string {
|
|
65
|
+
const row = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
|
|
66
|
+
return `${anthropicCooldownFlightKey(baseKey, accountId)}\0${row ? JSON.stringify([credentialGeneration(row.credential), row.loginId, row.addedAt]) : "missing"}`;
|
|
105
67
|
}
|
|
106
|
-
|
|
107
|
-
|
|
68
|
+
|
|
69
|
+
function reserveCooldownGeneration(accountId: string): number {
|
|
70
|
+
return cooldownGenerationByAccount?.get(accountId) ?? noteAnthropicCooldownMutation(accountId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function noteAnthropicCooldownMutation(accountId: string): number {
|
|
74
|
+
const generation = nextCooldownGeneration++;
|
|
75
|
+
(cooldownGenerationByAccount ??= new Map()).set(accountId, generation);
|
|
76
|
+
return generation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function anthropicAccountIncarnation(accountId: string): number {
|
|
80
|
+
return accountIncarnations?.get(accountId) ?? emptyAccountIncarnation;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function reserveAnthropicAccountIncarnation(accountId: string): number {
|
|
84
|
+
const existing = accountIncarnations?.get(accountId);
|
|
85
|
+
if (existing !== undefined) return existing;
|
|
86
|
+
const incarnation = nextAccountIncarnation++;
|
|
87
|
+
(accountIncarnations ??= new Map()).set(accountId, incarnation);
|
|
88
|
+
return incarnation;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clearAnthropicCooldownGenerations(): void {
|
|
92
|
+
emptyGeneration = nextCooldownGeneration++;
|
|
93
|
+
cooldownGenerationByAccount = undefined;
|
|
94
|
+
emptyAccountIncarnation = nextAccountIncarnation++;
|
|
95
|
+
accountIncarnations = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bind a quota probe to both the credential and cooldown generations it observed before
|
|
100
|
+
* dispatch. A successful response may otherwise arrive after either credential replacement
|
|
101
|
+
* or a newer 429 and incorrectly make unrelated state eligible.
|
|
102
|
+
*/
|
|
103
|
+
async function captureAnthropicCooldownRecoveryProbe(
|
|
104
|
+
accountId: string,
|
|
105
|
+
accessToken: string,
|
|
106
|
+
): Promise<AnthropicCooldownRecoveryProbe | null> {
|
|
107
|
+
const credential = getAccountCredential(instance, accountId);
|
|
108
|
+
if (!credential || credential.access !== accessToken) return null;
|
|
109
|
+
const generation = credentialGeneration(credential);
|
|
110
|
+
const incarnation = reserveAnthropicAccountIncarnation(accountId);
|
|
111
|
+
const quotaEpoch = captureProviderAccountQuotaEpoch(instance);
|
|
112
|
+
const initialRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
|
|
113
|
+
if (!initialRow || initialRow.paused || initialRow.needsReauth || credentialGeneration(initialRow.credential) !== generation) return null;
|
|
114
|
+
// Lazy because anthropic-routing reads the quota cache on normal request routing.
|
|
115
|
+
const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
|
|
116
|
+
const routing = anthropicRoutingFor(instance);
|
|
117
|
+
const live = getAccountCredential(instance, accountId);
|
|
118
|
+
if (!live || credentialGeneration(live) !== generation) return null;
|
|
119
|
+
const cooldownGeneration = reserveCooldownGeneration(accountId);
|
|
120
|
+
const capturedFamilyGeneration = anthropicModelQuotaFor(instance).captureAnthropicFamilyQuotaGeneration(accountId);
|
|
121
|
+
const claim = routing.captureAnthropicCooldownRecovery(accountId);
|
|
122
|
+
const isCurrentCredential = () => {
|
|
123
|
+
const currentRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
|
|
124
|
+
const current = currentRow?.credential;
|
|
125
|
+
return quotaEpoch === captureProviderAccountQuotaEpoch(instance)
|
|
126
|
+
&& anthropicAccountIncarnation(accountId) === incarnation
|
|
127
|
+
&& currentRow?.loginId === initialRow?.loginId && currentRow?.addedAt === initialRow?.addedAt
|
|
128
|
+
&& !!current && credentialGeneration(current) === generation;
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
instance,
|
|
132
|
+
requiresFreshDispatch: claim !== null,
|
|
133
|
+
isCurrentCredential,
|
|
134
|
+
isCurrentFamily: () => familyGeneration(accountId) === capturedFamilyGeneration,
|
|
135
|
+
isCurrent: () => isCurrentCredential()
|
|
136
|
+
&& anthropicCooldownGeneration(accountId) === cooldownGeneration,
|
|
137
|
+
settle: quota => !isCurrentCredential() || anthropicCooldownGeneration(accountId) !== cooldownGeneration ? "superseded"
|
|
138
|
+
: claim === null ? "retained" : routing.settleAnthropicCooldownRecovery(claim, quota),
|
|
139
|
+
};
|
|
108
140
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
|
|
142
|
+
/** Run one authoritative usage read and publish its recovery effect only while still owned. */
|
|
143
|
+
async function probeAnthropicQuotaWithRecovery(
|
|
144
|
+
accountId: string,
|
|
145
|
+
accessToken: string,
|
|
146
|
+
read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>,
|
|
147
|
+
mayPublish: () => boolean,
|
|
148
|
+
): Promise<AnthropicQuotaRecoveryResult | null> {
|
|
149
|
+
const probe = await captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
|
|
150
|
+
if (!probe) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost credential ownership`);
|
|
151
|
+
if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost dispatch ownership`);
|
|
152
|
+
let quota: ProviderQuota | null;
|
|
153
|
+
try { quota = await read(probe.requiresFreshDispatch); }
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe failure is stale`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (!probe.isCurrent() || !mayPublish()) {
|
|
159
|
+
throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe result is stale`);
|
|
160
|
+
}
|
|
161
|
+
if (!quota) return null;
|
|
162
|
+
const settlement = probe.settle(quota);
|
|
163
|
+
if (settlement === "superseded") {
|
|
164
|
+
throw new AnthropicQuotaProbeOwnershipError(`${instance} quota probe lost cooldown ownership`);
|
|
165
|
+
}
|
|
166
|
+
// Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
|
|
167
|
+
// generation; any later observation/429 then invalidates publication before a cache write.
|
|
168
|
+
const ownsFamily = probe.isCurrentFamily();
|
|
169
|
+
const authoritative = ownsFamily && hasAnthropicFamilyEnumeration(quota);
|
|
170
|
+
if (ownsFamily) anthropicModelQuotaFor(instance).observeAnthropicFamilyQuota(accountId, quota.customWindows ?? [], quota.updatedAt, authoritative);
|
|
171
|
+
if (!authoritative) {
|
|
172
|
+
const cached = getCachedProviderAccountQuota(instance, accountId)?.customWindows;
|
|
173
|
+
const windows = ownsFamily ? mergeAnthropicFamilyWindows(cached, quota.customWindows)
|
|
174
|
+
: cached ?? [];
|
|
175
|
+
// A superseded family enumeration has no authority, including over absent families.
|
|
176
|
+
if (!ownsFamily || windows.length) quota = { ...quota, customWindows: windows };
|
|
177
|
+
}
|
|
178
|
+
const publicationGeneration = anthropicCooldownGeneration(accountId);
|
|
179
|
+
const publicationFamilyGeneration = familyGeneration(accountId);
|
|
180
|
+
afterSettlementForTests?.();
|
|
181
|
+
return {
|
|
182
|
+
instance,
|
|
183
|
+
quota,
|
|
184
|
+
isCurrent: () => probe.isCurrentCredential()
|
|
185
|
+
&& anthropicCooldownGeneration(accountId) === publicationGeneration
|
|
186
|
+
&& familyGeneration(accountId) === publicationFamilyGeneration
|
|
187
|
+
&& mayPublish(),
|
|
188
|
+
};
|
|
113
189
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!ownsFamily || windows.length) quota = { ...quota, customWindows: windows };
|
|
190
|
+
|
|
191
|
+
function reconcileAnthropicCooldownGenerations(context: GenerationContext): number {
|
|
192
|
+
// Probe reservations include accounts with no cooldown, so every claim retires here.
|
|
193
|
+
for (const id of cooldownGenerationByAccount?.keys() ?? []) {
|
|
194
|
+
if (!context.oauthAccountKeys.has(`${instance}\0${id}`)) noteAnthropicCooldownMutation(id);
|
|
195
|
+
}
|
|
196
|
+
for (const id of accountIncarnations?.keys() ?? []) {
|
|
197
|
+
if (!context.oauthAccountKeys.has(`${instance}\0${id}`)) accountIncarnations!.set(id, nextAccountIncarnation++);
|
|
198
|
+
}
|
|
199
|
+
return 0; // Tombstones are retained, never reclaimed into an older generation.
|
|
125
200
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
201
|
+
return Object.freeze({ instance, assertAnthropicQuotaSendAllowed, setAnthropicQuotaAfterSettlementForTests,
|
|
202
|
+
anthropicCooldownGeneration, anthropicCooldownFlightKey, anthropicCredentialQuotaFlightKey, noteAnthropicCooldownMutation,
|
|
203
|
+
anthropicAccountIncarnation, reserveAnthropicAccountIncarnation,
|
|
204
|
+
clearAnthropicCooldownGenerations, captureAnthropicCooldownRecoveryProbe,
|
|
205
|
+
probeAnthropicQuotaWithRecovery, reconcileAnthropicCooldownGenerations });
|
|
206
|
+
}
|
|
207
|
+
export type AnthropicCooldownRecovery = ReturnType<typeof createAnthropicCooldownRecovery>;
|
|
208
|
+
const instances = new Map<AnthropicInstanceId, AnthropicCooldownRecovery>();
|
|
209
|
+
export function anthropicCooldownRecoveryFor(instance: AnthropicInstanceId): AnthropicCooldownRecovery {
|
|
210
|
+
let facade = instances.get(instance);
|
|
211
|
+
if (!facade) { facade = createAnthropicCooldownRecovery(instance); instances.set(instance, facade); }
|
|
212
|
+
return facade;
|
|
213
|
+
}
|
|
214
|
+
export function reconcileAllAnthropicCooldownGenerations(context: GenerationContext): number {
|
|
215
|
+
for (const facade of instances.values()) facade.reconcileAnthropicCooldownGenerations(context);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
export function clearAllAnthropicCooldownGenerations(): void {
|
|
219
|
+
for (const facade of instances.values()) facade.clearAnthropicCooldownGenerations();
|
|
220
|
+
}
|
|
221
|
+
export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
|
|
222
|
+
anthropicCooldownRecoveryFor("anthropic").assertAnthropicQuotaSendAllowed(accountId, token);
|
|
223
|
+
}
|
|
224
|
+
export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
|
|
225
|
+
anthropicCooldownRecoveryFor("anthropic").setAnthropicQuotaAfterSettlementForTests(hook);
|
|
226
|
+
}
|
|
227
|
+
export function anthropicCooldownGeneration(accountId: string): number {
|
|
228
|
+
return anthropicCooldownRecoveryFor("anthropic").anthropicCooldownGeneration(accountId);
|
|
229
|
+
}
|
|
230
|
+
export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
|
|
231
|
+
return anthropicCooldownRecoveryFor("anthropic").anthropicCooldownFlightKey(baseKey, accountId);
|
|
232
|
+
}
|
|
233
|
+
export function noteAnthropicCooldownMutation(accountId: string): number {
|
|
234
|
+
return anthropicCooldownRecoveryFor("anthropic").noteAnthropicCooldownMutation(accountId);
|
|
235
|
+
}
|
|
236
|
+
export function clearAnthropicCooldownGenerations(): void {
|
|
237
|
+
anthropicCooldownRecoveryFor("anthropic").clearAnthropicCooldownGenerations();
|
|
238
|
+
}
|
|
239
|
+
export function captureAnthropicCooldownRecoveryProbe(accountId: string, accessToken: string): Promise<AnthropicCooldownRecoveryProbe | null> {
|
|
240
|
+
return anthropicCooldownRecoveryFor("anthropic").captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
|
|
241
|
+
}
|
|
242
|
+
export function probeAnthropicQuotaWithRecovery(
|
|
243
|
+
accountId: string, accessToken: string,
|
|
244
|
+
read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>, mayPublish: () => boolean,
|
|
245
|
+
): Promise<AnthropicQuotaRecoveryResult | null> {
|
|
246
|
+
return anthropicCooldownRecoveryFor("anthropic").probeAnthropicQuotaWithRecovery(accountId, accessToken, read, mayPublish);
|
|
136
247
|
}
|
|
@@ -8,7 +8,7 @@ import { apiKeyPoolEntryId } from "../api-keys";
|
|
|
8
8
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../registry";
|
|
9
9
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../openai-tiers";
|
|
10
10
|
import { CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type CodexCapacityAggregation, type CodexCapacityQuota } from "../codex-capacity";
|
|
11
|
-
import { clearCachedProviderQuotas, providerQuotaRoutingBinding, type ProviderQuotaRoutingEvidence } from "../quota-routing-cache";
|
|
11
|
+
import { clearCachedProviderQuotas, providerQuotaRoutingBinding, replaceCachedProviderQuotas, type ProviderQuotaRoutingEvidence } from "../quota-routing-cache";
|
|
12
12
|
import { clearProviderApiKeyQuotaCache } from "../quota-key-accounts";
|
|
13
13
|
import { QUOTA_JSON_READ_FAILURE, readQuotaJson } from "../quota-wire";
|
|
14
14
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
@@ -101,6 +101,29 @@ export function clearProviderQuotaCache(): void {
|
|
|
101
101
|
invalidationEpoch += 1;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const providerReportEpochs = new Map<string, number>();
|
|
105
|
+
|
|
106
|
+
export function captureProviderReportEpochs(): ReadonlyMap<string, number> {
|
|
107
|
+
return new Map(providerReportEpochs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function providerReportEpochChanged(captured: ReadonlyMap<string, number>, provider: string): boolean {
|
|
111
|
+
return (providerReportEpochs.get(provider) ?? 0) !== (captured.get(provider) ?? 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Provider-scoped invalidation: drop one provider's report and routing row. The global epoch
|
|
116
|
+
* is untouched, so another provider's cached row and in-flight commit authority survive; an
|
|
117
|
+
* aggregate already in flight drops only this provider's row when it publishes.
|
|
118
|
+
*/
|
|
119
|
+
export function clearProviderQuotaCacheFor(provider: string): void {
|
|
120
|
+
providerReportEpochs.set(provider, (providerReportEpochs.get(provider) ?? 0) + 1);
|
|
121
|
+
if (!cache) return;
|
|
122
|
+
const reports = cache.response.reports.filter(report => report.provider !== provider);
|
|
123
|
+
cache = { ...cache, response: { ...cache.response, reports } };
|
|
124
|
+
replaceCachedProviderQuotas(reports, routingEvidence);
|
|
125
|
+
}
|
|
126
|
+
|
|
104
127
|
function cacheKey(config: OcxConfig): string {
|
|
105
128
|
const providers = Object.entries(config.providers)
|
|
106
129
|
.map(([name, provider]) => {
|
|
@@ -38,6 +38,7 @@ const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
|
|
38
38
|
const CLINE_BASE_URL = "https://api.cline.bot";
|
|
39
39
|
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
|
|
40
40
|
const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
|
|
41
|
+
const OLLAMA_CLOUD_BALANCE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/balance`;
|
|
41
42
|
const ZAI_BASE_URL = "https://api.z.ai";
|
|
42
43
|
const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
|
|
43
44
|
const MINIMAX_REMAINS_PATH = "/v1/api/openplatform/coding_plan/remains";
|
|
@@ -439,11 +440,80 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
|
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
/**
|
|
442
|
-
* Ollama Cloud `GET https://ollama.com/api/
|
|
443
|
-
*
|
|
444
|
-
* `
|
|
445
|
-
* `
|
|
443
|
+
* Ollama Cloud quota. `GET https://ollama.com/api/balance` is the live source: it
|
|
444
|
+
* returns the included (plan) balance plus purchased credits. Legacy plans report
|
|
445
|
+
* rolling 5-hour `included.session.remaining_percent` and 7-day
|
|
446
|
+
* `included.weekly.remaining_percent` (remaining shares, so used = 100 - remaining);
|
|
447
|
+
* credit plans report `included.balance_usd`/`included.allowance_usd` over a
|
|
448
|
+
* `period`.
|
|
449
|
+
*
|
|
450
|
+
* `GET https://ollama.com/api/usage` used to carry `limits.{session,weekly,monthly}.usage`
|
|
451
|
+
* as normalized fractions but now returns only a request-count timeseries, so it is
|
|
452
|
+
* kept as a fallback for older deployments.
|
|
446
453
|
*/
|
|
454
|
+
function parseOllamaUsedPercent(row: Record<string, unknown>): number | undefined {
|
|
455
|
+
if (row.remaining_percent !== undefined) {
|
|
456
|
+
const remaining = toFiniteNumber(row.remaining_percent);
|
|
457
|
+
if (remaining === undefined) return undefined;
|
|
458
|
+
return normalizePercent(Math.round((100 - remaining) * 100) / 100);
|
|
459
|
+
}
|
|
460
|
+
return parseOllamaPercent(row.usage);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function parseOllamaCloudBalance(body: Record<string, unknown> | null): ProviderQuota | null {
|
|
464
|
+
if (!body) return null;
|
|
465
|
+
const included = asRecord(body.included);
|
|
466
|
+
if (!included) return null;
|
|
467
|
+
|
|
468
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
469
|
+
let windows = 0;
|
|
470
|
+
|
|
471
|
+
const rows: Array<[string, "fiveHour" | "weekly" | "monthly"]> = [
|
|
472
|
+
["session", "fiveHour"],
|
|
473
|
+
["weekly", "weekly"],
|
|
474
|
+
["monthly", "monthly"],
|
|
475
|
+
];
|
|
476
|
+
for (const [key, window] of rows) {
|
|
477
|
+
const row = asRecord(included[key]);
|
|
478
|
+
if (!row) continue;
|
|
479
|
+
const percent = parseOllamaUsedPercent(row);
|
|
480
|
+
if (percent === undefined) continue;
|
|
481
|
+
const resetAt = normalizeResetAt(row.resets_at);
|
|
482
|
+
if (window === "fiveHour") {
|
|
483
|
+
quota.fiveHourPercent = percent;
|
|
484
|
+
if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
|
|
485
|
+
} else if (window === "weekly") {
|
|
486
|
+
quota.weeklyPercent = percent;
|
|
487
|
+
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
488
|
+
} else {
|
|
489
|
+
quota.monthlyPercent = percent;
|
|
490
|
+
if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
|
|
491
|
+
}
|
|
492
|
+
windows += 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const allowance = toFiniteNumber(included.allowance_usd);
|
|
496
|
+
const balance = toFiniteNumber(included.balance_usd);
|
|
497
|
+
if (allowance !== undefined && allowance > 0 && balance !== undefined) {
|
|
498
|
+
const used = Math.max(0, allowance - balance);
|
|
499
|
+
const percent = normalizePercent(Math.round((used / allowance) * 10000) / 100);
|
|
500
|
+
if (percent !== undefined) {
|
|
501
|
+
const expiresAt = normalizeResetAt(asRecord(included.period)?.until);
|
|
502
|
+
// creditsUsd is the included allowance only; purchased credits are ignored.
|
|
503
|
+
quota.creditsUsd = {
|
|
504
|
+
used,
|
|
505
|
+
limit: allowance,
|
|
506
|
+
remaining: balance,
|
|
507
|
+
percent,
|
|
508
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
509
|
+
};
|
|
510
|
+
windows += 1;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return windows > 0 ? quota : null;
|
|
515
|
+
}
|
|
516
|
+
|
|
447
517
|
function parseOllamaPercent(usageValue: unknown): number | undefined {
|
|
448
518
|
const usage = toFiniteNumber(usageValue);
|
|
449
519
|
if (usage === undefined || usage < 0) return undefined;
|
|
@@ -489,25 +559,65 @@ export function parseOllamaCloudQuota(body: Record<string, unknown> | null): Pro
|
|
|
489
559
|
return windows > 0 ? quota : null;
|
|
490
560
|
}
|
|
491
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Routing projection for an `/api/balance` report. `creditsUsd` covers the included
|
|
564
|
+
* allowance only, so it may veto routing only when purchased credit is known to be empty;
|
|
565
|
+
* otherwise an exhausted allowance would block an account that still has paid balance.
|
|
566
|
+
*/
|
|
567
|
+
function ollamaBalanceInferenceQuota(quota: ProviderQuota, body: Record<string, unknown> | null): ProviderQuota {
|
|
568
|
+
if (!quota.creditsUsd) return quota;
|
|
569
|
+
const purchased = toFiniteNumber(asRecord(body?.purchased)?.balance_usd);
|
|
570
|
+
if (purchased !== undefined && purchased <= 0) return quota;
|
|
571
|
+
const { creditsUsd: _includedOnly, ...inference } = quota;
|
|
572
|
+
return inference;
|
|
573
|
+
}
|
|
574
|
+
|
|
492
575
|
async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
493
576
|
const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
|
|
494
577
|
if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
|
|
495
578
|
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
496
579
|
if (!apiKey) return null;
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
580
|
+
const attempts: Array<{
|
|
581
|
+
url: string;
|
|
582
|
+
source: string;
|
|
583
|
+
parse: (body: Record<string, unknown> | null) => ProviderQuota | null;
|
|
584
|
+
}> = [
|
|
585
|
+
{ url: OLLAMA_CLOUD_BALANCE_URL, source: "ollama-cloud:balance", parse: parseOllamaCloudBalance },
|
|
586
|
+
{ url: OLLAMA_CLOUD_USAGE_URL, source: "ollama-cloud:usage", parse: parseOllamaCloudQuota },
|
|
587
|
+
];
|
|
588
|
+
let terminalFailure = false;
|
|
589
|
+
for (const attempt of attempts) {
|
|
590
|
+
let response: Response;
|
|
591
|
+
try {
|
|
592
|
+
response = await quotaFetch(provider, config, attempt.url, {
|
|
593
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
594
|
+
redirect: "error",
|
|
595
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
596
|
+
});
|
|
597
|
+
} catch {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (!response.ok) {
|
|
601
|
+
if (response.status !== 404 && response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) {
|
|
602
|
+
terminalFailure = true;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
void response.body?.cancel().catch(() => undefined);
|
|
606
|
+
} catch {
|
|
607
|
+
// Best-effort cancellation only.
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const raw = await readQuotaJson(response);
|
|
612
|
+
if (raw === QUOTA_JSON_READ_FAILURE) return terminalFailure ? TERMINAL_QUOTA_FAILURE : null;
|
|
613
|
+
const body = asRecord(raw);
|
|
614
|
+
const quota = attempt.parse(body);
|
|
615
|
+
if (quota) {
|
|
616
|
+
const inference = attempt.url === OLLAMA_CLOUD_BALANCE_URL ? ollamaBalanceInferenceQuota(quota, body) : quota;
|
|
617
|
+
return keyReport(provider, attempt.source, quota, config, apiKey, inference);
|
|
618
|
+
}
|
|
507
619
|
}
|
|
508
|
-
|
|
509
|
-
const quota = parseOllamaCloudQuota(body);
|
|
510
|
-
return quota ? keyReport(provider, "ollama-cloud:usage", quota, config, apiKey, quota) : null;
|
|
620
|
+
return terminalFailure ? TERMINAL_QUOTA_FAILURE : null;
|
|
511
621
|
}
|
|
512
622
|
|
|
513
623
|
/**
|