@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
|
@@ -21,6 +21,7 @@ import { existsSync, mkdirSync, readFileSync, writeSync } from "node:fs";
|
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { atomicWriteFileStreamed } from "../config/atomic-write";
|
|
23
23
|
import { getConfigDir } from "../config/paths";
|
|
24
|
+
import type { AnthropicInstanceId } from "./anthropic-instance-id";
|
|
24
25
|
|
|
25
26
|
export const ANTHROPIC_RESET_LEASE_MS = 90_000;
|
|
26
27
|
export const ANTHROPIC_RESET_RETRY_WINDOW_MS = 600_000;
|
|
@@ -83,7 +84,16 @@ export function anthropicOrgDigest(organizationUuid: string): string {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function anthropicResetJournalPath(customDir?: string): string {
|
|
86
|
-
return
|
|
87
|
+
return anthropicResetJournalPathForInstance("anthropic", customDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function anthropicResetJournalPathForInstance(instance: AnthropicInstanceId, customDir?: string): string {
|
|
91
|
+
return join(customDir ?? getConfigDir(), `${instance}-reset-grant-ledger.json`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Namespace is in the path; A's version-1 journal bytes and record schema stay unchanged. */
|
|
95
|
+
export function anthropicResetLedgerOptionsForInstance(instance: AnthropicInstanceId, customDir?: string): AnthropicResetLedgerOptions {
|
|
96
|
+
return { journalPath: anthropicResetJournalPathForInstance(instance, customDir) };
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
function isRecordShape(value: unknown): value is OperationRecord {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { atomicWriteFile } from "../config/atomic-write";
|
|
4
|
+
import { getConfigDir } from "../config/paths";
|
|
5
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
6
|
+
|
|
7
|
+
const SOURCE_URL = "https://models.dev/api.json";
|
|
8
|
+
const FILENAME = "azure-model-metadata-cache.json";
|
|
9
|
+
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export type AzureModelMetadata = { input?: string[]; contextWindow?: number; maxTokens?: number };
|
|
12
|
+
type Snapshot = { version: 1; fetchedAt: number; models: Record<string, AzureModelMetadata> };
|
|
13
|
+
type CacheState = { snapshot?: Snapshot | null; inFlight?: Promise<void>; retryAfter: number };
|
|
14
|
+
const cacheByConfigDir = new Map<string, CacheState>();
|
|
15
|
+
|
|
16
|
+
function cacheState(configDir: string): CacheState {
|
|
17
|
+
let state = cacheByConfigDir.get(configDir);
|
|
18
|
+
if (!state) {
|
|
19
|
+
state = { retryAfter: 0 };
|
|
20
|
+
cacheByConfigDir.set(configDir, state);
|
|
21
|
+
}
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAzureModelMetadataDestination(baseUrl: string | undefined): boolean {
|
|
26
|
+
try {
|
|
27
|
+
return !!baseUrl && new URL(baseUrl).hostname.endsWith(".openai.azure.com");
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
34
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
39
|
+
? value as Record<string, unknown> : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseModels(models: unknown, published: boolean): Record<string, AzureModelMetadata> {
|
|
43
|
+
const out: Record<string, AzureModelMetadata> = Object.create(null);
|
|
44
|
+
for (const [id, value] of Object.entries(record(models) ?? {}).slice(0, 2000)) {
|
|
45
|
+
const row = record(value);
|
|
46
|
+
if (!row || id.length > 256) continue;
|
|
47
|
+
const limit = published ? record(row.limit) : row;
|
|
48
|
+
const input = published ? record(row.modalities)?.input : row.input;
|
|
49
|
+
const modalities = Array.isArray(input)
|
|
50
|
+
? [...new Set(input.filter((item): item is string => item === "text" || item === "image" || item === "audio"))]
|
|
51
|
+
: [];
|
|
52
|
+
const contextWindow = positiveInteger(published ? limit?.context : limit?.contextWindow);
|
|
53
|
+
const maxTokens = positiveInteger(published ? limit?.output : limit?.maxTokens);
|
|
54
|
+
if (!modalities.length && contextWindow === undefined && maxTokens === undefined) continue;
|
|
55
|
+
out[id.toLowerCase()] = {
|
|
56
|
+
...(modalities.length ? { input: modalities } : {}),
|
|
57
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
58
|
+
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadSnapshot(configDir: string, state: CacheState): Snapshot | null {
|
|
65
|
+
if (state.snapshot !== undefined) return state.snapshot;
|
|
66
|
+
state.snapshot = null;
|
|
67
|
+
try {
|
|
68
|
+
const raw = record(JSON.parse(readFileSync(join(configDir, FILENAME), "utf8")));
|
|
69
|
+
if (raw?.version === 1 && positiveInteger(raw.fetchedAt) !== undefined && record(raw.models)) {
|
|
70
|
+
state.snapshot = { version: 1, fetchedAt: raw.fetchedAt as number, models: parseModels(raw.models, false) };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* Missing or corrupt optional metadata leaves bundled hints available. */ }
|
|
73
|
+
return state.snapshot;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Synchronous catalog fallback. Generation requests never fetch this snapshot. */
|
|
77
|
+
export function publishedAzureModelMetadata(modelId: string, configDir = getConfigDir()): AzureModelMetadata | undefined {
|
|
78
|
+
return loadSnapshot(configDir, cacheState(configDir))?.models[modelId.toLowerCase()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Refresh once per day during discovery, coalesced and bounded; retain stale data offline. */
|
|
82
|
+
export async function refreshAzureModelMetadata(baseUrl: string | undefined, configDir = getConfigDir()): Promise<void> {
|
|
83
|
+
if (!isAzureModelMetadataDestination(baseUrl)) return;
|
|
84
|
+
// Capture state and destination together: tests and embedders can change config roots.
|
|
85
|
+
const state = cacheState(configDir);
|
|
86
|
+
const current = loadSnapshot(configDir, state);
|
|
87
|
+
if ((current && Date.now() - current.fetchedAt < TTL_MS) || Date.now() < state.retryAfter) return;
|
|
88
|
+
if (state.inFlight) return state.inFlight;
|
|
89
|
+
const path = join(configDir, FILENAME);
|
|
90
|
+
state.inFlight = (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const signal = AbortSignal.timeout(2000);
|
|
93
|
+
const response = await fetch(SOURCE_URL, {
|
|
94
|
+
headers: { accept: "application/json" },
|
|
95
|
+
redirect: "error",
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) return;
|
|
99
|
+
const body = await readBoundedResponseBytes(response, { maxBytes: 16 * 1024 * 1024, signal });
|
|
100
|
+
if (body.oversized) return;
|
|
101
|
+
const raw = record(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body.bytes)));
|
|
102
|
+
// Provider-specific rows matter: Azure may expose text only where the direct vendor has vision.
|
|
103
|
+
const models = parseModels(record(raw?.azure)?.models, true);
|
|
104
|
+
if (!Object.keys(models).length) return;
|
|
105
|
+
state.snapshot = { version: 1, fetchedAt: Date.now(), models };
|
|
106
|
+
try { atomicWriteFile(path, JSON.stringify(state.snapshot) + "\n"); } catch { /* Memory still serves discovery. */ }
|
|
107
|
+
} catch { /* A public metadata outage must not fail provider discovery. */ }
|
|
108
|
+
finally { state.retryAfter = Date.now() + 60_000; }
|
|
109
|
+
})().finally(() => { state.inFlight = undefined; });
|
|
110
|
+
return state.inFlight;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resetAzureModelMetadataForTests(): void {
|
|
114
|
+
cacheByConfigDir.clear();
|
|
115
|
+
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
|
+
import { COST_VENDOR_PRIORITY, getModelMetadata, getModelMetadataCaseInsensitive } from "../generated/model-metadata";
|
|
2
3
|
import { cloneFastWire } from "./fastwire";
|
|
4
|
+
import { isAzureModelMetadataDestination, publishedAzureModelMetadata, type AzureModelMetadata } from "./azure-model-metadata";
|
|
3
5
|
import { resolveModelPolicy } from "./resolved-model-policy";
|
|
4
6
|
import {
|
|
5
7
|
PROVIDER_REGISTRY,
|
|
@@ -245,6 +247,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
245
247
|
// Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
|
|
246
248
|
// distinguish local runtimes from API-key providers keep working after the seed round-trip.
|
|
247
249
|
authMode: entry.authKind,
|
|
250
|
+
...(entry.id === "anthropic2" && entry.authKind === "oauth" && entry.oauthFamily === "anthropic"
|
|
251
|
+
? { anthropicOAuthInstance: "anthropic2" as const } : {}),
|
|
248
252
|
...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
|
|
249
253
|
...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
|
|
250
254
|
...(entry.freeTier !== undefined ? { freeTier: entry.freeTier } : {}),
|
|
@@ -649,6 +653,25 @@ export function shouldCaseFoldMetadataModelId(providerId: string): boolean {
|
|
|
649
653
|
return entry?.metadataModelIdNormalize === "case-insensitive";
|
|
650
654
|
}
|
|
651
655
|
|
|
656
|
+
const AZURE_VENDOR_METADATA_PRIORITY = [
|
|
657
|
+
"azure-openai",
|
|
658
|
+
...COST_VENDOR_PRIORITY.filter(provider => provider !== "azure-openai"),
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Fill missing Azure model metadata by destination and known model id. Custom provider
|
|
663
|
+
* names miss PROVIDER_ALIASES; arbitrary deployment aliases still need explicit metadata.
|
|
664
|
+
*/
|
|
665
|
+
export function azureVendorModelMetadata(baseUrl: string | undefined, modelId: string, metadataConfigDir?: string): AzureModelMetadata | undefined {
|
|
666
|
+
if (!isAzureModelMetadataDestination(baseUrl)) return undefined;
|
|
667
|
+
const published = publishedAzureModelMetadata(modelId, metadataConfigDir);
|
|
668
|
+
for (const provider of AZURE_VENDOR_METADATA_PRIORITY) {
|
|
669
|
+
const meta = getModelMetadata(provider, modelId) ?? getModelMetadataCaseInsensitive(provider, modelId);
|
|
670
|
+
if (meta) return { ...meta, ...published };
|
|
671
|
+
}
|
|
672
|
+
return published;
|
|
673
|
+
}
|
|
674
|
+
|
|
652
675
|
function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
|
|
653
676
|
return {
|
|
654
677
|
id: entry.id,
|
|
@@ -25,6 +25,8 @@ export const KIRO_MODELS = [
|
|
|
25
25
|
"claude-sonnet-4.6",
|
|
26
26
|
"claude-sonnet-4.5",
|
|
27
27
|
"claude-sonnet-4.0",
|
|
28
|
+
// 261008 preemptive: Haiku 5.5; calls fail upstream until Kiro ships it.
|
|
29
|
+
"claude-haiku-5.5",
|
|
28
30
|
"claude-haiku-4.5",
|
|
29
31
|
"deepseek-3.2",
|
|
30
32
|
"minimax-m2.5",
|
|
@@ -55,6 +57,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
55
57
|
"claude-sonnet-4.6": 1_000_000,
|
|
56
58
|
"claude-sonnet-4.5": 200_000,
|
|
57
59
|
"claude-sonnet-4.0": 200_000,
|
|
60
|
+
"claude-haiku-5.5": 1_000_000,
|
|
58
61
|
"claude-haiku-4.5": 200_000,
|
|
59
62
|
"deepseek-3.2": 128_000,
|
|
60
63
|
"minimax-m2.5": 200_000,
|
package/src/providers/label.ts
CHANGED
|
@@ -89,6 +89,6 @@ export function stampOAuthAccountLabel(
|
|
|
89
89
|
if (!accountId) return;
|
|
90
90
|
if (provider.authMode !== "oauth") return;
|
|
91
91
|
const base = baseProviderLabel(providerName);
|
|
92
|
-
if (base === "openai" || base === "anthropic") return;
|
|
92
|
+
if (base === "openai" || (base === "anthropic" || base === "anthropic2")) return;
|
|
93
93
|
logCtx.accountLogLabel = oauthAccountLogLabel(accountId, base);
|
|
94
94
|
}
|
|
@@ -78,6 +78,8 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
|
|
|
78
78
|
modelVercelGatewayRouting: "record",
|
|
79
79
|
authMode: "none",
|
|
80
80
|
oauthAccountFailover: "none",
|
|
81
|
+
anthropicAccountPool: "none",
|
|
82
|
+
anthropicOAuthInstance: "none",
|
|
81
83
|
keyOptional: "none",
|
|
82
84
|
freeTier: "none",
|
|
83
85
|
note: "none",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { parseAnthropicFamilyHeaders, mergeAnthropicFamilyWindows } from "./anthropic-family-headers";
|
|
2
|
-
import {
|
|
2
|
+
import { anthropicModelQuotaFor, ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS } from "../../oauth/anthropic-model-quota";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { getValidAccessTokenForAccount } from "../../oauth";
|
|
5
5
|
import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store";
|
|
6
|
+
import { isAnthropicInstanceId, type AnthropicInstanceId } from "../anthropic-instance-id";
|
|
6
7
|
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
7
8
|
import { ACCOUNT_QUOTA_TTL_MS, toFiniteNumber } from "../quota-wire";
|
|
8
9
|
import { clearKiroAccountUsageState, hydrateKiroUsageVerdict, kiroPersistableVerdicts, reconcileKiroAccountUsageState } from "../kiro-usage";
|
|
@@ -96,8 +97,19 @@ export function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined,
|
|
|
96
97
|
return hasQuotaRows(result) ? result : null;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function isAnthropicAccountCacheKey(key: string): boolean {
|
|
101
|
+
const separator = key.indexOf("\0");
|
|
102
|
+
return separator > 0 && isAnthropicInstanceId(key.slice(0, separator));
|
|
103
|
+
}
|
|
104
|
+
|
|
99
105
|
export const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
|
|
100
106
|
export let explicitAccountEpoch = 0;
|
|
107
|
+
let allAccountQuotaEpoch = 0;
|
|
108
|
+
const providerAccountQuotaEpochs = new Map<string, number>();
|
|
109
|
+
/** Instance-owned cache clears do not revoke an unrelated account's probe. */
|
|
110
|
+
export function captureProviderAccountQuotaEpoch(provider: string): number {
|
|
111
|
+
return allAccountQuotaEpoch + (providerAccountQuotaEpochs.get(provider) ?? 0);
|
|
112
|
+
}
|
|
101
113
|
|
|
102
114
|
/**
|
|
103
115
|
* Seed the cache from the last run, once.
|
|
@@ -127,7 +139,7 @@ export function hydrateAccountQuotaCache(): void {
|
|
|
127
139
|
}
|
|
128
140
|
// Disk stores observation time, not the Anthropic usage probe's clock.
|
|
129
141
|
if (!accountQuotaCache.has(key)) {
|
|
130
|
-
const anthropic = key
|
|
142
|
+
const anthropic = isAnthropicAccountCacheKey(key);
|
|
131
143
|
accountQuotaCache.set(key, {
|
|
132
144
|
ts: anthropic ? 0 : quota.updatedAt,
|
|
133
145
|
quota: anthropic ? normalizeAnthropicQuota(quota, now)
|
|
@@ -148,7 +160,7 @@ export function persistAccountQuotaCache(): void {
|
|
|
148
160
|
const liveKiro = new Map<string, ProviderAccount>(getAccountSet("kiro")?.accounts.map(account =>
|
|
149
161
|
[accountCacheKey("kiro", account.id), account]) ?? []);
|
|
150
162
|
for (const [key, entry] of accountQuotaCache) {
|
|
151
|
-
const quota = key
|
|
163
|
+
const quota = isAnthropicAccountCacheKey(key) ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
|
|
152
164
|
if (quota && key.startsWith("kiro\0")) {
|
|
153
165
|
const account = liveKiro.get(key);
|
|
154
166
|
if (account && entry.identity === kiroEvidenceIdentity(account))
|
|
@@ -182,7 +194,7 @@ export interface ProviderAccountQuota {
|
|
|
182
194
|
|
|
183
195
|
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
184
196
|
export function supportsPerAccountQuota(provider: string): boolean {
|
|
185
|
-
return provider
|
|
197
|
+
return isAnthropicInstanceId(provider) || provider === "kiro" || provider === "google-antigravity"
|
|
186
198
|
|| explicitAccountReader(provider);
|
|
187
199
|
}
|
|
188
200
|
|
|
@@ -209,12 +221,12 @@ export function getCachedProviderAccountQuota(provider: string, accountId: strin
|
|
|
209
221
|
const account = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
|
|
210
222
|
if (!account || entry?.identity !== kiroEvidenceIdentity(account)) return null;
|
|
211
223
|
}
|
|
212
|
-
if (provider
|
|
224
|
+
if (isAnthropicInstanceId(provider) && entry?.anthropicCredentialGeneration) {
|
|
213
225
|
const credential = getAccountCredential(provider, accountId);
|
|
214
226
|
if (!credential || credentialGeneration(credential) !== entry.anthropicCredentialGeneration) return null;
|
|
215
227
|
}
|
|
216
228
|
if (entry?.isCurrent && !entry.isCurrent()) return null;
|
|
217
|
-
return provider
|
|
229
|
+
return isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
/** Test-only: seed or clear the per-account quota cache without probing upstream. */
|
|
@@ -270,7 +282,8 @@ function normalizeUtilizationFraction(value: string | null): number | undefined
|
|
|
270
282
|
* erasing model-specific windows. The caller owns credential attribution; this guard
|
|
271
283
|
* prevents a retired account key from being revived by an older config generation.
|
|
272
284
|
*/
|
|
273
|
-
export function
|
|
285
|
+
export function recordAnthropicAccountQuotaFromHeadersForInstance(
|
|
286
|
+
instance: AnthropicInstanceId,
|
|
274
287
|
accountId: string,
|
|
275
288
|
headers: Headers,
|
|
276
289
|
writerGeneration: number,
|
|
@@ -278,7 +291,8 @@ export function recordAnthropicAccountQuotaFromHeaders(
|
|
|
278
291
|
model?: string,
|
|
279
292
|
): void {
|
|
280
293
|
if (!accountId) return;
|
|
281
|
-
const key = accountCacheKey(
|
|
294
|
+
const key = accountCacheKey(instance, accountId);
|
|
295
|
+
const { observeAnthropicFamilyQuota, clearAnthropicRequestedFamilyQuota } = anthropicModelQuotaFor(instance);
|
|
282
296
|
if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
|
|
283
297
|
if (status !== undefined && status >= 200 && status < 300) clearAnthropicRequestedFamilyQuota(accountId, model);
|
|
284
298
|
const observed = parseAnthropicRateLimitHeaders(headers, status);
|
|
@@ -289,7 +303,7 @@ export function recordAnthropicAccountQuotaFromHeaders(
|
|
|
289
303
|
// every other provider's saved row.
|
|
290
304
|
hydrateAccountQuotaCache();
|
|
291
305
|
const candidate = accountQuotaCache.get(key);
|
|
292
|
-
const credential = getAccountCredential(
|
|
306
|
+
const credential = getAccountCredential(instance, accountId);
|
|
293
307
|
const generation = credential && credentialGeneration(credential);
|
|
294
308
|
const previous = candidate?.isCurrent?.() === false
|
|
295
309
|
|| candidate?.anthropicCredentialGeneration && candidate.anthropicCredentialGeneration !== generation ? undefined : candidate;
|
|
@@ -307,6 +321,13 @@ export function recordAnthropicAccountQuotaFromHeaders(
|
|
|
307
321
|
persistAccountQuotaCache();
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
/** Legacy recorder: config/roster generation is numeric and distinct from bearer ownership. */
|
|
325
|
+
export function recordAnthropicAccountQuotaFromHeaders(
|
|
326
|
+
accountId: string, headers: Headers, writerGeneration: number, status?: number, model?: string,
|
|
327
|
+
): void {
|
|
328
|
+
recordAnthropicAccountQuotaFromHeadersForInstance("anthropic", accountId, headers, writerGeneration, status, model);
|
|
329
|
+
}
|
|
330
|
+
|
|
310
331
|
/**
|
|
311
332
|
* Providers whose per-account quota is OBSERVED in-band, never probed.
|
|
312
333
|
*
|
|
@@ -380,7 +401,7 @@ export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
|
|
|
380
401
|
let removed = 0;
|
|
381
402
|
for (const [key, entry] of accountQuotaCache) {
|
|
382
403
|
// Anthropic observations extend retention, never the usage probe's eligibility clock.
|
|
383
|
-
const retainedAt = key
|
|
404
|
+
const retainedAt = isAnthropicAccountCacheKey(key)
|
|
384
405
|
? Math.max(entry.ts, entry.quota?.updatedAt ?? 0)
|
|
385
406
|
: entry.ts;
|
|
386
407
|
if (retainedAt + ACCOUNT_QUOTA_TTL_MS > now) continue;
|
|
@@ -425,8 +446,10 @@ export function resetProviderQuotaReconcileStateForTests(): void {
|
|
|
425
446
|
|
|
426
447
|
/** Drop cached per-account rows (all, or just one provider's). */
|
|
427
448
|
export function clearAccountQuotaCache(provider?: string): void {
|
|
428
|
-
|
|
449
|
+
// Anthropic readers use provider-owned epochs; B cannot revoke an A flight.
|
|
450
|
+
if (provider !== "anthropic2") explicitAccountEpoch += 1;
|
|
429
451
|
if (!provider) {
|
|
452
|
+
allAccountQuotaEpoch += 1;
|
|
430
453
|
accountQuotaCache.clear();
|
|
431
454
|
accountQuotaInflight.clear();
|
|
432
455
|
clearKiroAccountUsageState();
|
|
@@ -436,6 +459,7 @@ export function clearAccountQuotaCache(provider?: string): void {
|
|
|
436
459
|
cancelPendingAccountQuotaPersist();
|
|
437
460
|
return;
|
|
438
461
|
}
|
|
462
|
+
providerAccountQuotaEpochs.set(provider, (providerAccountQuotaEpochs.get(provider) ?? 0) + 1);
|
|
439
463
|
hydrateAccountQuotaCache();
|
|
440
464
|
const prefix = `${provider}\u0000`;
|
|
441
465
|
for (const key of [...accountQuotaCache.keys()]) {
|
|
@@ -464,7 +488,9 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
|
|
|
464
488
|
const row = getAccountCredentialWithStatus(provider, accountId);
|
|
465
489
|
if (!row) throw new Error("account credential missing");
|
|
466
490
|
// An operator-paused account is excluded from every automatic upstream use, quota reads included.
|
|
491
|
+
// Anthropic instances also skip a needs-reauth row; other providers keep their probe behavior.
|
|
467
492
|
if (row.paused) throw new Error("account is paused; quota probe skipped");
|
|
493
|
+
if (row.needsReauth && isAnthropicInstanceId(provider)) throw new Error("account needs sign-in; quota probe skipped");
|
|
468
494
|
const stored = row.credential;
|
|
469
495
|
if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
|
|
470
496
|
const activeId = getAccountSet(provider)?.activeAccountId;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { loadConfig } from "../../config";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
import { getAccountSet } from "../../oauth/store";
|
|
4
|
+
import { captureConfigGeneration, sweepExpiredOnWrite } from "../../lib/state-store-sweeper";
|
|
5
|
+
import { configuredAnthropicInstance, type AnthropicInstanceId } from "../anthropic-instance";
|
|
6
|
+
import { ACCOUNT_QUOTA_TTL_MS } from "../quota-wire";
|
|
7
|
+
import { fetchAnthropicUsageQuotaForInstance } from "./vendor-probes-oauth";
|
|
8
|
+
import { AnthropicQuotaProbeOwnershipError, anthropicCooldownRecoveryFor, type AnthropicQuotaRecoveryResult } from "./anthropic-cooldown-recovery";
|
|
9
|
+
import {
|
|
10
|
+
accountCacheKey, accountQuotaCache, accountQuotaInflight, captureProviderAccountQuotaEpoch,
|
|
11
|
+
getTokenForAccountQuotaProbe, hydrateAccountQuotaCache, mayCommitAccountQuotaKey,
|
|
12
|
+
normalizeAnthropicQuota, type AccountQuotaCacheEntry,
|
|
13
|
+
} from "./account-cache";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve renewal first: a flight always belongs to the credential actually dispatched.
|
|
17
|
+
* `config` is the caller's live object. Currency re-reads that one object, never a fresh file
|
|
18
|
+
* load, so memory/disk drift cannot refuse every probe; commit authority stays with the
|
|
19
|
+
* captured writer generation.
|
|
20
|
+
*/
|
|
21
|
+
export async function fetchAnthropicAccountQuota(
|
|
22
|
+
instance: AnthropicInstanceId, accountId: string, force: boolean, config: OcxConfig = loadConfig(),
|
|
23
|
+
): Promise<AccountQuotaCacheEntry> {
|
|
24
|
+
const unavailable = (): AccountQuotaCacheEntry => ({ ts: Date.now(), quota: null, unavailable: true });
|
|
25
|
+
const target = config.providers[instance];
|
|
26
|
+
if (target?.disabled || target && target.authMode !== "oauth" || configuredAnthropicInstance(config, instance) !== instance) return unavailable();
|
|
27
|
+
const targetIdentity = JSON.stringify(target);
|
|
28
|
+
const epoch = captureProviderAccountQuotaEpoch(instance);
|
|
29
|
+
const initialRow = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
|
|
30
|
+
if (!initialRow) return unavailable();
|
|
31
|
+
const recovery = anthropicCooldownRecoveryFor(instance);
|
|
32
|
+
const incarnation = recovery.reserveAnthropicAccountIncarnation(accountId);
|
|
33
|
+
const ownerCurrent = () => {
|
|
34
|
+
const row = getAccountSet(instance)?.accounts.find(row => row.id === accountId);
|
|
35
|
+
return epoch === captureProviderAccountQuotaEpoch(instance)
|
|
36
|
+
&& configuredAnthropicInstance(config, instance) === instance
|
|
37
|
+
&& JSON.stringify(config.providers[instance]) === targetIdentity
|
|
38
|
+
&& recovery.anthropicAccountIncarnation(accountId) === incarnation
|
|
39
|
+
&& !!row && row.loginId === initialRow.loginId && row.addedAt === initialRow.addedAt;
|
|
40
|
+
};
|
|
41
|
+
hydrateAccountQuotaCache();
|
|
42
|
+
const key = accountCacheKey(instance, accountId);
|
|
43
|
+
const candidate = accountQuotaCache.get(key);
|
|
44
|
+
const cached = candidate?.isCurrent?.() === false ? undefined : candidate;
|
|
45
|
+
if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
|
|
46
|
+
return { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) };
|
|
47
|
+
}
|
|
48
|
+
let token: string;
|
|
49
|
+
try { token = await getTokenForAccountQuotaProbe(instance, accountId); }
|
|
50
|
+
catch { return unavailable(); }
|
|
51
|
+
if (!ownerCurrent()) return unavailable();
|
|
52
|
+
const flightKey = recovery.anthropicCredentialQuotaFlightKey(key, accountId);
|
|
53
|
+
const flightCurrent = () => ownerCurrent()
|
|
54
|
+
&& recovery.anthropicCredentialQuotaFlightKey(key, accountId) === flightKey;
|
|
55
|
+
const joinable = accountQuotaInflight.get(flightKey);
|
|
56
|
+
if (joinable) {
|
|
57
|
+
const joined = await joinable;
|
|
58
|
+
return ownerCurrent() && (joined.isCurrent?.() ?? flightCurrent()) ? joined : unavailable();
|
|
59
|
+
}
|
|
60
|
+
const writerGeneration = captureConfigGeneration();
|
|
61
|
+
// One publication path for a result and for a failed dispatch. A null result (HTTP refusal)
|
|
62
|
+
// and a thrown read (network failure) both record the unavailable row over the retained
|
|
63
|
+
// in-flight observations, as the pre-instance A path did, so joined callers and later cached
|
|
64
|
+
// reads agree and windows that expired during the shared probe are normalized away.
|
|
65
|
+
const publish = (result: AnthropicQuotaRecoveryResult | null): AccountQuotaCacheEntry => {
|
|
66
|
+
const previous = accountQuotaCache.get(key);
|
|
67
|
+
const retained = previous?.isCurrent?.() === false ? undefined : previous;
|
|
68
|
+
const isCurrent = result?.isCurrent ?? flightCurrent;
|
|
69
|
+
const entry: AccountQuotaCacheEntry = {
|
|
70
|
+
ts: Date.now(), quota: normalizeAnthropicQuota(result?.quota ?? retained?.quota, Date.now()),
|
|
71
|
+
...(!result ? { unavailable: true as const } : {}), isCurrent,
|
|
72
|
+
};
|
|
73
|
+
if (isCurrent() && mayCommitAccountQuotaKey(key, writerGeneration)) { accountQuotaCache.set(key, entry); sweepExpiredOnWrite(entry.ts); }
|
|
74
|
+
return entry;
|
|
75
|
+
};
|
|
76
|
+
const probe = (async (): Promise<AccountQuotaCacheEntry> => {
|
|
77
|
+
try {
|
|
78
|
+
const result = await recovery.probeAnthropicQuotaWithRecovery(accountId, token, fresh => {
|
|
79
|
+
if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("quota flight changed");
|
|
80
|
+
recovery.assertAnthropicQuotaSendAllowed(accountId, token);
|
|
81
|
+
return fetchAnthropicUsageQuotaForInstance(instance, token, fresh);
|
|
82
|
+
}, () => ownerCurrent() && mayCommitAccountQuotaKey(key, writerGeneration));
|
|
83
|
+
if (result && !result.isCurrent()) return unavailable();
|
|
84
|
+
if (!result && !flightCurrent()) return unavailable();
|
|
85
|
+
return publish(result);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof AnthropicQuotaProbeOwnershipError || !flightCurrent()) return unavailable();
|
|
88
|
+
return publish(null);
|
|
89
|
+
}
|
|
90
|
+
})().finally(() => { if (accountQuotaInflight.get(flightKey) === probe) accountQuotaInflight.delete(flightKey); });
|
|
91
|
+
accountQuotaInflight.set(flightKey, probe);
|
|
92
|
+
return probe;
|
|
93
|
+
}
|