@bitkyc08/opencodex 2.72.0 → 2.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
- package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
- package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
- package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/native-exec.ts +28 -10
- package/src/adapters/devin/live-models.ts +5 -0
- package/src/adapters/kiro/reasoning.ts +2 -0
- package/src/adapters/openai-chat/messages.ts +4 -1
- package/src/adapters/openai-responses/passthrough.ts +12 -1
- package/src/adapters/openai-responses/reasoning.ts +15 -0
- package/src/bridge/internal.ts +12 -0
- package/src/cli/account-anthropic-threshold.ts +41 -0
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +32 -21
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +8 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/build-entries.ts +6 -5
- package/src/codex/catalog/derive-entry.ts +3 -4
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/parsing.ts +4 -1
- package/src/codex/catalog/pinned-models.ts +3 -1
- package/src/codex/catalog/provider-models.ts +22 -2
- package/src/codex/data/roster-pinned-models.json +178 -1
- package/src/codex/model-entitlements.ts +1 -1
- package/src/codex/native-profile-store.ts +2 -1
- package/src/combos/resolve.ts +16 -1
- package/src/config/subagent-models.ts +30 -5
- package/src/generated/compatibility-version.json +95 -67
- package/src/generated/model-metadata.ts +4 -4
- package/src/lib/account-selection-events.ts +28 -0
- package/src/lib/errors.ts +7 -5
- package/src/lib/keyring-native.ts +132 -0
- package/src/lib/provider-outbound.ts +13 -0
- package/src/lib/retry-delay.ts +10 -0
- package/src/oauth/anthropic-account-threshold.ts +13 -0
- package/src/oauth/anthropic-routing.ts +223 -52
- package/src/oauth/index.ts +4 -2
- package/src/oauth/store.ts +45 -3
- package/src/oauth/types.ts +2 -0
- package/src/providers/api-key-resolve.ts +2 -4
- package/src/providers/codebuddy-models.ts +6 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/label.ts +20 -1
- package/src/providers/quota/account-cache.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
- package/src/providers/quota/vendor-probes-oauth.ts +84 -57
- package/src/providers/quota.ts +45 -46
- package/src/providers/registry/entries-core.ts +3 -1
- package/src/providers/registry/entries-extended.ts +14 -1
- package/src/providers/registry/model-seeds.ts +7 -1
- package/src/server/claude-messages.ts +84 -27
- package/src/server/management/anthropic-account-threshold.ts +27 -0
- package/src/server/management/oauth-account-routes.ts +17 -4
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/messages-native-oauth.ts +20 -21
- package/src/server/messages-native.ts +39 -14
- package/src/server/relay-eager.ts +5 -2
- package/src/server/relay.ts +268 -82
- package/src/server/responses/adapter-continuation.ts +6 -0
- package/src/server/responses/adapter-delivery.ts +25 -7
- package/src/server/responses/adapter-dispatch.ts +31 -0
- package/src/server/responses/buffered-sse-json.ts +345 -0
- package/src/server/responses/core-codex-account.ts +4 -1
- package/src/server/responses/core-combo.ts +28 -0
- package/src/server/responses/core-lifetime.ts +4 -0
- package/src/server/responses/passthrough-delivery.ts +305 -10
- package/src/server/responses/passthrough-dispatch.ts +60 -25
- package/src/server/responses/passthrough-error.ts +3 -1
- package/src/server/responses/request-transport.ts +22 -5
- package/src/server/responses/terminal-error-redaction.ts +53 -0
- package/src/service/windows-taskxml.ts +20 -6
- package/src/service-manager-probe.ts +115 -6
- package/src/types/wire.ts +7 -0
- package/src/usage/expected-prices.ts +11 -0
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* Policy: a reference that cannot be resolved fails closed (no key) and is warned once per
|
|
17
17
|
* account; nothing ever rewrites plaintext into config or its backups.
|
|
18
18
|
*/
|
|
19
|
-
import { createRequire } from "node:module";
|
|
20
19
|
import { resolveEnvValue } from "../config/proxy-env";
|
|
20
|
+
import { loadKeyringBinding } from "../lib/keyring-native";
|
|
21
21
|
import type { OcxProviderConfig } from "../types";
|
|
22
22
|
|
|
23
23
|
export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
|
|
@@ -31,10 +31,8 @@ export interface ProviderKeychainEntry {
|
|
|
31
31
|
|
|
32
32
|
export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
|
|
33
33
|
|
|
34
|
-
const nodeRequire = createRequire(import.meta.url);
|
|
35
|
-
|
|
36
34
|
function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
|
|
37
|
-
const { Entry } =
|
|
35
|
+
const { Entry } = loadKeyringBinding() as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
|
|
38
36
|
return new Entry(service, account);
|
|
39
37
|
}
|
|
40
38
|
|
|
@@ -28,6 +28,8 @@ export const CODEBUDDY_GLOBAL_MODELS = [
|
|
|
28
28
|
// 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows.
|
|
29
29
|
"gpt-6-sol",
|
|
30
30
|
"gpt-6-luna",
|
|
31
|
+
// 260930 preemptive: GPT-6.1 Sol (OpenAI announced 2026-09-29); mirrors the GPT-6 Sol row.
|
|
32
|
+
"gpt-6.1-sol",
|
|
31
33
|
"gpt-5.5",
|
|
32
34
|
"gpt-5.4",
|
|
33
35
|
"gpt-5.3-codex",
|
|
@@ -79,6 +81,7 @@ export const CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
79
81
|
"gpt-5.6-luna": 1_000_000,
|
|
80
82
|
"gpt-6-sol": 1_000_000,
|
|
81
83
|
"gpt-6-luna": 1_000_000,
|
|
84
|
+
"gpt-6.1-sol": 1_000_000,
|
|
82
85
|
"gpt-5.5": 1_000_000,
|
|
83
86
|
"gpt-5.4": 272_000,
|
|
84
87
|
"gpt-5.3-codex": 272_000,
|
|
@@ -101,6 +104,7 @@ export const CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> =
|
|
|
101
104
|
"gpt-5.6-luna": 128_000,
|
|
102
105
|
"gpt-6-sol": 128_000,
|
|
103
106
|
"gpt-6-luna": 128_000,
|
|
107
|
+
"gpt-6.1-sol": 128_000,
|
|
104
108
|
"gpt-5.5": 72_000,
|
|
105
109
|
"gpt-5.4": 128_000,
|
|
106
110
|
"gpt-5.3-codex": 128_000,
|
|
@@ -119,6 +123,7 @@ export const CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS: Record<string, string[]>
|
|
|
119
123
|
"gpt-5.6-luna": ["low", "medium", "high", "xhigh"],
|
|
120
124
|
"gpt-6-sol": ["low", "medium", "high", "xhigh"],
|
|
121
125
|
"gpt-6-luna": ["low", "medium", "high", "xhigh"],
|
|
126
|
+
"gpt-6.1-sol": ["low", "medium", "high", "xhigh"],
|
|
122
127
|
"glm-5.3": ["low", "high", "max"],
|
|
123
128
|
"glm-5.2": ["high", "xhigh"],
|
|
124
129
|
};
|
|
@@ -129,6 +134,7 @@ export const CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, st
|
|
|
129
134
|
"gpt-5.6-luna": "high",
|
|
130
135
|
"gpt-6-sol": "high",
|
|
131
136
|
"gpt-6-luna": "high",
|
|
137
|
+
"gpt-6.1-sol": "high",
|
|
132
138
|
"glm-5.3": "high",
|
|
133
139
|
"glm-5.2": "high",
|
|
134
140
|
};
|
|
@@ -7,6 +7,9 @@ export const KIRO_MODELS = [
|
|
|
7
7
|
// 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows. Calls fail upstream until Kiro ships the models.
|
|
8
8
|
"gpt-6-sol",
|
|
9
9
|
"gpt-6-luna",
|
|
10
|
+
// 260930 preemptive: GPT-6.1 Sol (OpenAI announced 2026-09-29; kiro.dev did not list it on 2026-09-30).
|
|
11
|
+
// Mirrors gpt-6-sol; calls fail upstream until Kiro ships the model.
|
|
12
|
+
"gpt-6.1-sol",
|
|
10
13
|
// 260929 preemptive: Claude Sonnet 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on
|
|
11
14
|
// 2026-09-29). Mirrors claude-sonnet-5; calls fail upstream until Kiro ships the model.
|
|
12
15
|
"claude-sonnet-5.5",
|
|
@@ -40,6 +43,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
40
43
|
"gpt-5.6-luna": 1_000_000,
|
|
41
44
|
"gpt-6-sol": 272_000,
|
|
42
45
|
"gpt-6-luna": 272_000,
|
|
46
|
+
"gpt-6.1-sol": 272_000,
|
|
43
47
|
"claude-sonnet-5.5": 1_000_000,
|
|
44
48
|
"claude-sonnet-5": 1_000_000,
|
|
45
49
|
"claude-opus-5.5": 1_000_000,
|
package/src/providers/label.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
|
|
1
|
+
import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE, KEY_ACCOUNT_LOG_LABEL_RE, apiKeyAccountLogLabel, oauthAccountLogLabel } from "../codex/account-label";
|
|
2
2
|
import type { OcxProviderConfig } from "../types";
|
|
3
3
|
|
|
4
4
|
export function canonicalUsageProviderLabel(provider: string): string {
|
|
@@ -25,6 +25,25 @@ export function stampApiKeyAccountLabel(
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The account-qualified provider label a pool dispatch stamped for this request, or undefined.
|
|
30
|
+
*
|
|
31
|
+
* An account pool rewrites the logged provider to name the account that served the turn
|
|
32
|
+
* (`anthropic-p2e22d0`, `openai-main`); the configured provider name never does, so a label that
|
|
33
|
+
* differs from the configured one AND ends in an account label identifies exactly one account.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT `accountLogLabel`: that is stamped for a lone configured API key too, so it
|
|
36
|
+
* cannot tell a pooled account apart from a provider that has no pool to rotate within.
|
|
37
|
+
*/
|
|
38
|
+
export function poolAccountProviderLabel(
|
|
39
|
+
loggedProvider: string | undefined,
|
|
40
|
+
configuredProvider: string,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
if (!loggedProvider?.startsWith(`${configuredProvider}-`)) return undefined;
|
|
43
|
+
return ACCOUNT_LOG_LABEL_RE.test(loggedProvider.slice(configuredProvider.length + 1))
|
|
44
|
+
? loggedProvider : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
export function baseProviderLabel(provider: string): string {
|
|
29
48
|
const canonical = canonicalUsageProviderLabel(provider);
|
|
30
49
|
if (canonical !== provider) return canonical;
|
|
@@ -272,7 +272,8 @@ export function recordAnthropicAccountQuotaFromHeaders(
|
|
|
272
272
|
// whole map. Landing before any reader has hydrated would persist this single row and erase
|
|
273
273
|
// every other provider's saved row.
|
|
274
274
|
hydrateAccountQuotaCache();
|
|
275
|
-
const
|
|
275
|
+
const candidate = accountQuotaCache.get(key);
|
|
276
|
+
const previous = candidate?.isCurrent?.() === false ? undefined : candidate;
|
|
276
277
|
accountQuotaCache.set(key, {
|
|
277
278
|
...previous,
|
|
278
279
|
// Headers do not prove that the last usage probe succeeded.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus } from "../../oauth/store";
|
|
2
|
+
import type { ProviderQuota } from "../quota-types";
|
|
3
|
+
|
|
4
|
+
export class AnthropicQuotaProbeOwnershipError extends Error {}
|
|
5
|
+
|
|
6
|
+
/** Check live pause and bearer ownership at the last synchronous step before usage dispatch. */
|
|
7
|
+
export function assertAnthropicQuotaSendAllowed(accountId: string, token: string): void {
|
|
8
|
+
const row = getAccountCredentialWithStatus("anthropic", accountId);
|
|
9
|
+
if (!row || row.paused || row.needsReauth || row.credential.access !== token) {
|
|
10
|
+
throw new AnthropicQuotaProbeOwnershipError("anthropic quota account is no longer eligible");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cooldownGenerationByAccount = new Map<string, number>();
|
|
15
|
+
let nextCooldownGeneration = 1;
|
|
16
|
+
let afterSettlementForTests: (() => void) | undefined;
|
|
17
|
+
|
|
18
|
+
export function setAnthropicQuotaAfterSettlementForTests(hook: (() => void) | undefined): void {
|
|
19
|
+
afterSettlementForTests = hook;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Monotonic per-account fence shared by routing mutations and quota publication. */
|
|
23
|
+
export function anthropicCooldownGeneration(accountId: string): number {
|
|
24
|
+
return cooldownGenerationByAccount.get(accountId) ?? 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Keep same-state quota calls joinable while moving post-429 calls to a fresh flight. */
|
|
28
|
+
export function anthropicCooldownFlightKey(baseKey: string, accountId: string): string {
|
|
29
|
+
return `${baseKey}\0anthropic-cooldown:${anthropicCooldownGeneration(accountId)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function noteAnthropicCooldownMutation(accountId: string): number {
|
|
33
|
+
const generation = nextCooldownGeneration++;
|
|
34
|
+
cooldownGenerationByAccount.set(accountId, generation);
|
|
35
|
+
return generation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function clearAnthropicCooldownGenerations(): void {
|
|
39
|
+
cooldownGenerationByAccount.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AnthropicCooldownRecoveryProbe = Readonly<{
|
|
43
|
+
requiresFreshDispatch: boolean;
|
|
44
|
+
isCurrentCredential(): boolean;
|
|
45
|
+
isCurrent(): boolean;
|
|
46
|
+
settle(quota: ProviderQuota): "cleared" | "retained" | "superseded";
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export type AnthropicQuotaRecoveryResult = Readonly<{
|
|
50
|
+
quota: ProviderQuota;
|
|
51
|
+
/** Must be checked synchronously at each publication boundary. */
|
|
52
|
+
isCurrent(): boolean;
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bind a quota probe to both the credential and cooldown generations it observed before
|
|
57
|
+
* dispatch. A successful response may otherwise arrive after either credential replacement
|
|
58
|
+
* or a newer 429 and incorrectly make unrelated state eligible.
|
|
59
|
+
*/
|
|
60
|
+
export async function captureAnthropicCooldownRecoveryProbe(
|
|
61
|
+
accountId: string,
|
|
62
|
+
accessToken: string,
|
|
63
|
+
): Promise<AnthropicCooldownRecoveryProbe | null> {
|
|
64
|
+
const credential = getAccountCredential("anthropic", accountId);
|
|
65
|
+
if (!credential || credential.access !== accessToken) return null;
|
|
66
|
+
const generation = credentialGeneration(credential);
|
|
67
|
+
// Lazy because anthropic-routing reads the quota cache on normal request routing.
|
|
68
|
+
const routing = await import("../../oauth/anthropic-routing");
|
|
69
|
+
const cooldownGeneration = anthropicCooldownGeneration(accountId);
|
|
70
|
+
const claim = routing.captureAnthropicCooldownRecovery(accountId);
|
|
71
|
+
const isCurrentCredential = () => {
|
|
72
|
+
const current = getAccountCredential("anthropic", accountId);
|
|
73
|
+
return !!current && credentialGeneration(current) === generation;
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
requiresFreshDispatch: claim !== null,
|
|
77
|
+
isCurrentCredential,
|
|
78
|
+
isCurrent: () => isCurrentCredential()
|
|
79
|
+
&& anthropicCooldownGeneration(accountId) === cooldownGeneration,
|
|
80
|
+
settle: quota => claim === null ? "retained"
|
|
81
|
+
: routing.settleAnthropicCooldownRecovery(claim, quota),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Run one authoritative usage read and publish its recovery effect only while still owned. */
|
|
86
|
+
export async function probeAnthropicQuotaWithRecovery(
|
|
87
|
+
accountId: string,
|
|
88
|
+
accessToken: string,
|
|
89
|
+
read: (requireFreshDispatch: boolean) => Promise<ProviderQuota | null>,
|
|
90
|
+
mayPublish: () => boolean,
|
|
91
|
+
): Promise<AnthropicQuotaRecoveryResult | null> {
|
|
92
|
+
const probe = await captureAnthropicCooldownRecoveryProbe(accountId, accessToken);
|
|
93
|
+
if (!probe) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost credential ownership");
|
|
94
|
+
let quota: ProviderQuota | null;
|
|
95
|
+
try { quota = await read(probe.requiresFreshDispatch); }
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!probe.isCurrent() || !mayPublish()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe failure is stale");
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (!probe.isCurrent() || !mayPublish()) {
|
|
101
|
+
throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe result is stale");
|
|
102
|
+
}
|
|
103
|
+
if (!quota) return null;
|
|
104
|
+
const settlement = probe.settle(quota);
|
|
105
|
+
if (settlement === "superseded") {
|
|
106
|
+
throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost cooldown ownership");
|
|
107
|
+
}
|
|
108
|
+
// Clearing the claimed cooldown intentionally advances the fence. Adopt that exact new
|
|
109
|
+
// generation; any later observation/429 then invalidates publication before a cache write.
|
|
110
|
+
const publicationGeneration = anthropicCooldownGeneration(accountId);
|
|
111
|
+
afterSettlementForTests?.();
|
|
112
|
+
return {
|
|
113
|
+
quota,
|
|
114
|
+
isCurrent: () => probe.isCurrentCredential()
|
|
115
|
+
&& anthropicCooldownGeneration(accountId) === publicationGeneration
|
|
116
|
+
&& mayPublish(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
|
|
2
2
|
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
3
3
|
import { getValidAccessToken } from "../../oauth";
|
|
4
|
-
import { getAccountCredential, getAccountSet } from "../../oauth/store";
|
|
4
|
+
import { captureOAuthAccountSelection, getAccountCredential, getAccountSet } from "../../oauth/store";
|
|
5
5
|
import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
|
|
6
6
|
import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
|
|
7
7
|
import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
|
|
@@ -18,6 +18,7 @@ import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type Codex
|
|
|
18
18
|
import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
|
|
19
19
|
import { providerCodexAccountMode } from "../registry";
|
|
20
20
|
import {
|
|
21
|
+
accountReportCurrent,
|
|
21
22
|
TERMINAL_QUOTA_FAILURE,
|
|
22
23
|
hasQuotaRows,
|
|
23
24
|
providerLabel,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
} from "./account-cache";
|
|
39
40
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
40
41
|
import type { ProviderQuota, ProviderQuotaWindow } from "../quota-types";
|
|
42
|
+
import { AnthropicQuotaProbeOwnershipError, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./anthropic-cooldown-recovery";
|
|
41
43
|
|
|
42
44
|
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
|
|
43
45
|
const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
|
|
@@ -262,6 +264,56 @@ function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
|
|
|
262
264
|
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
263
265
|
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
264
266
|
|
|
267
|
+
async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
|
|
268
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
269
|
+
headers: {
|
|
270
|
+
Accept: "application/json, text/plain, */*",
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
"User-Agent": CLAUDE_CLI_USER_AGENT,
|
|
273
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
|
274
|
+
Authorization: `Bearer ${accessToken}`,
|
|
275
|
+
},
|
|
276
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) return null;
|
|
279
|
+
const body = asRecord(await readQuotaJson(response));
|
|
280
|
+
if (!body) return null;
|
|
281
|
+
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
282
|
+
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
283
|
+
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
284
|
+
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
285
|
+
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
286
|
+
const customWindows: ProviderQuotaWindow[] = [];
|
|
287
|
+
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
288
|
+
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
289
|
+
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
290
|
+
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
291
|
+
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
292
|
+
for (const rawLimit of limits) {
|
|
293
|
+
const limitRecord = asRecord(rawLimit);
|
|
294
|
+
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
295
|
+
// buckets above; only model-scoped weekly limits add a third window.
|
|
296
|
+
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
297
|
+
const limit = parseClaudeLimit(rawLimit);
|
|
298
|
+
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
299
|
+
knownLabels.add(limit.label.toLowerCase());
|
|
300
|
+
customWindows.push(limit);
|
|
301
|
+
}
|
|
302
|
+
const quota: ProviderQuota = {
|
|
303
|
+
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
304
|
+
// rows: report it in the canonical fields so the dashboard renders it with the standard
|
|
305
|
+
// "5-hour limit" label and ordering instead of as a generic extra window.
|
|
306
|
+
...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
|
|
307
|
+
...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
308
|
+
...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
|
|
309
|
+
...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
|
|
310
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
311
|
+
updatedAt: Date.now(),
|
|
312
|
+
};
|
|
313
|
+
// Empty / schema-changed payloads must not cache as "success with no bars".
|
|
314
|
+
return hasQuotaRows(quota) ? quota : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
265
317
|
/**
|
|
266
318
|
* Anthropic per-credential usage.
|
|
267
319
|
*
|
|
@@ -277,59 +329,16 @@ const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
|
277
329
|
* model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
|
|
278
330
|
* upstream returns the tier itself.
|
|
279
331
|
*/
|
|
280
|
-
export async function fetchAnthropicUsageQuota(
|
|
281
|
-
|
|
332
|
+
export async function fetchAnthropicUsageQuota(
|
|
333
|
+
accessToken: string,
|
|
334
|
+
requireFreshDispatch = false,
|
|
335
|
+
): Promise<ProviderQuota | null> {
|
|
336
|
+
// Recovery evidence must be requested after the claimed cooldown. Joining an older request
|
|
337
|
+
// can return after the 429 while still describing provider state from before that refusal.
|
|
338
|
+
const joinable = requireFreshDispatch ? undefined : anthropicUsageInflight.get(accessToken);
|
|
282
339
|
if (joinable) return joinable;
|
|
283
340
|
|
|
284
|
-
const probe = (
|
|
285
|
-
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
286
|
-
headers: {
|
|
287
|
-
Accept: "application/json, text/plain, */*",
|
|
288
|
-
"Content-Type": "application/json",
|
|
289
|
-
"User-Agent": CLAUDE_CLI_USER_AGENT,
|
|
290
|
-
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
|
|
291
|
-
Authorization: `Bearer ${accessToken}`,
|
|
292
|
-
},
|
|
293
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
294
|
-
});
|
|
295
|
-
if (!response.ok) return null;
|
|
296
|
-
const body = asRecord(await readQuotaJson(response));
|
|
297
|
-
if (!body) return null;
|
|
298
|
-
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
299
|
-
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
300
|
-
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
301
|
-
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
302
|
-
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
303
|
-
const customWindows: ProviderQuotaWindow[] = [];
|
|
304
|
-
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
305
|
-
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
306
|
-
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
307
|
-
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
308
|
-
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
309
|
-
for (const rawLimit of limits) {
|
|
310
|
-
const limitRecord = asRecord(rawLimit);
|
|
311
|
-
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
312
|
-
// buckets above; only model-scoped weekly limits add a third window.
|
|
313
|
-
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
314
|
-
const limit = parseClaudeLimit(rawLimit);
|
|
315
|
-
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
316
|
-
knownLabels.add(limit.label.toLowerCase());
|
|
317
|
-
customWindows.push(limit);
|
|
318
|
-
}
|
|
319
|
-
const quota: ProviderQuota = {
|
|
320
|
-
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
321
|
-
// rows: report it in the canonical fields so the dashboard renders it with the standard
|
|
322
|
-
// "5-hour limit" label and ordering instead of as a generic extra window.
|
|
323
|
-
...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
|
|
324
|
-
...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
325
|
-
...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
|
|
326
|
-
...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
|
|
327
|
-
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
328
|
-
updatedAt: Date.now(),
|
|
329
|
-
};
|
|
330
|
-
// Empty / schema-changed payloads must not cache as "success with no bars".
|
|
331
|
-
return hasQuotaRows(quota) ? quota : null;
|
|
332
|
-
})().finally(() => {
|
|
341
|
+
const probe = readAnthropicUsageQuota(accessToken).finally(() => {
|
|
333
342
|
if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
|
|
334
343
|
});
|
|
335
344
|
anthropicUsageInflight.set(accessToken, probe);
|
|
@@ -339,7 +348,8 @@ export async function fetchAnthropicUsageQuota(accessToken: string): Promise<Pro
|
|
|
339
348
|
export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
340
349
|
// Capture the account we intend to probe before awaiting — a mid-flight active
|
|
341
350
|
// switch must not seed the wrong account's cache with this response.
|
|
342
|
-
const
|
|
351
|
+
const selection = captureOAuthAccountSelection("anthropic");
|
|
352
|
+
const probedAccountId = selection?.accountId;
|
|
343
353
|
const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
|
|
344
354
|
const writerGeneration = captureConfigGeneration();
|
|
345
355
|
let accessToken: string;
|
|
@@ -348,17 +358,34 @@ export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuo
|
|
|
348
358
|
} catch {
|
|
349
359
|
return null;
|
|
350
360
|
}
|
|
351
|
-
|
|
361
|
+
if (!probedAccountId || !probedAccountKey) return null;
|
|
362
|
+
let quota: ProviderQuota | null;
|
|
363
|
+
let anthropicCurrent: (() => boolean) | undefined;
|
|
364
|
+
try {
|
|
365
|
+
const result = await probeAnthropicQuotaWithRecovery(probedAccountId, accessToken,
|
|
366
|
+
fresh => { assertAnthropicQuotaSendAllowed(probedAccountId, accessToken); return fetchAnthropicUsageQuota(accessToken, fresh); },
|
|
367
|
+
() => mayCommitAccountQuotaKey(probedAccountKey, writerGeneration));
|
|
368
|
+
if (result && !result.isCurrent()) return null;
|
|
369
|
+
quota = result?.quota ?? null;
|
|
370
|
+
anthropicCurrent = result?.isCurrent;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error instanceof AnthropicQuotaProbeOwnershipError) return null;
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
352
375
|
if (!quota) return null;
|
|
353
376
|
// Share the active-account probe with the per-account cache so Providers-page
|
|
354
377
|
// loads do not double-hit Anthropic's rate-limited usage endpoint.
|
|
355
378
|
if (probedAccountId && probedAccountKey) {
|
|
356
379
|
const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
|
|
357
|
-
if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
358
|
-
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
|
|
380
|
+
if (stillOwnsToken && anthropicCurrent?.() && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
381
|
+
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota, isCurrent: anthropicCurrent });
|
|
359
382
|
}
|
|
360
383
|
}
|
|
361
|
-
|
|
384
|
+
const quotaReport = report(provider, "anthropic:oauth-usage", quota);
|
|
385
|
+
if (quotaReport && anthropicCurrent) {
|
|
386
|
+
accountReportCurrent.set(quotaReport, () => anthropicCurrent() && getAccountSet("anthropic")?.activeAccountId === probedAccountId);
|
|
387
|
+
}
|
|
388
|
+
return quotaReport;
|
|
362
389
|
}
|
|
363
390
|
|
|
364
391
|
/**
|
package/src/providers/quota.ts
CHANGED
|
@@ -68,7 +68,7 @@ import { fetchCommandCodeQuota, fetchKimiQuota, keyQuotaReaderForProvider } from
|
|
|
68
68
|
import { antigravityQuotaDiagnosticIdentity, fetchAntigravityQuota, probeAntigravityUsageQuota } from "./quota/antigravity";
|
|
69
69
|
import { persistKiroAccountState } from "./kiro-account-state-disk";
|
|
70
70
|
import { kiroProbeCurrent, kiroProbeIdentity } from "./quota/kiro-account-probe";
|
|
71
|
-
|
|
71
|
+
import { AnthropicQuotaProbeOwnershipError, anthropicCooldownFlightKey, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./quota/anthropic-cooldown-recovery";
|
|
72
72
|
export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
|
|
73
73
|
export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
|
|
74
74
|
export {
|
|
@@ -98,7 +98,6 @@ export {
|
|
|
98
98
|
export { fetchAntigravityUsageQuota, isCanonicalAntigravityQuotaUrl, setAntigravityAccountQuotaTransportForTests } from "./quota/antigravity";
|
|
99
99
|
export { parseOllamaCloudQuota, parseZaiQuotaLimits, providerApiKeyQuotaMode } from "./quota/vendor-probes-key";
|
|
100
100
|
export { parseXaiCreditsResponse } from "./quota/vendor-probes-oauth";
|
|
101
|
-
|
|
102
101
|
export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string, forceRefresh = false): Promise<ProviderApiKeyQuota[]> {
|
|
103
102
|
const provider = config.providers[name];
|
|
104
103
|
if (!provider || !keyQuotaReaderForProvider(name, provider)) return [];
|
|
@@ -109,7 +108,6 @@ export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string,
|
|
|
109
108
|
return result ? { kind: "quota", quota: result.quota } : { kind: "unavailable" };
|
|
110
109
|
});
|
|
111
110
|
}
|
|
112
|
-
|
|
113
111
|
async function maybeFetchProviderQuota(
|
|
114
112
|
name: string,
|
|
115
113
|
provider: OcxProviderConfig,
|
|
@@ -338,7 +336,6 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
|
|
|
338
336
|
if (inflight.get(key) === entry) inflight.delete(key);
|
|
339
337
|
}
|
|
340
338
|
}
|
|
341
|
-
|
|
342
339
|
async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
|
|
343
340
|
result: ProviderQuotaProbeResult;
|
|
344
341
|
identity: string | undefined;
|
|
@@ -367,7 +364,6 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con
|
|
|
367
364
|
}
|
|
368
365
|
return { result, identity, isCurrent };
|
|
369
366
|
}
|
|
370
|
-
|
|
371
367
|
async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
|
|
372
368
|
const key = accountCacheKey(provider, accountId);
|
|
373
369
|
const identity = explicitQuotaIdentity(provider, accountId, configured);
|
|
@@ -403,7 +399,6 @@ async function fetchExplicitAccountQuota(provider: string, accountId: string, fo
|
|
|
403
399
|
accountQuotaInflight.set(flightKey, flight);
|
|
404
400
|
return flight;
|
|
405
401
|
}
|
|
406
|
-
|
|
407
402
|
async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
|
|
408
403
|
const id = getAccountSet(provider)?.activeAccountId;
|
|
409
404
|
if (!id) return null;
|
|
@@ -428,18 +423,22 @@ async function fetchAccountQuota(
|
|
|
428
423
|
const writerGeneration = captureConfigGeneration();
|
|
429
424
|
const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
|
|
430
425
|
const cachedCandidate = accountQuotaCache.get(key);
|
|
431
|
-
const cached = provider !== "kiro" || cachedCandidate?.identity === kiroIdentity
|
|
426
|
+
const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
|
|
427
|
+
&& (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined;
|
|
432
428
|
if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
|
|
433
429
|
if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
|
|
434
430
|
return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
|
|
435
431
|
}
|
|
436
|
-
const
|
|
432
|
+
const flightKey = provider === "anthropic" ? anthropicCooldownFlightKey(key, accountId) : key;
|
|
433
|
+
const flightCurrent = () => provider !== "anthropic" || anthropicCooldownFlightKey(key, accountId) === flightKey;
|
|
434
|
+
const joinable = accountQuotaInflight.get(flightKey);
|
|
437
435
|
if (joinable) {
|
|
438
436
|
const joined = await joinable;
|
|
437
|
+
const joinedCurrent = joined.isCurrent?.();
|
|
438
|
+
if (joinedCurrent === false || (!flightCurrent() && joinedCurrent !== true)) return { ts: Date.now(), quota: null, unavailable: true };
|
|
439
439
|
return provider !== "kiro" || joined.identity === kiroIdentity
|
|
440
440
|
? joined : fetchAccountQuota(provider, accountId, true, providerConfig);
|
|
441
441
|
}
|
|
442
|
-
|
|
443
442
|
const epoch = explicitAccountEpoch;
|
|
444
443
|
const probe = (async (): Promise<AccountQuotaCacheEntry> => {
|
|
445
444
|
let diagnosticIdentity: string | undefined;
|
|
@@ -449,15 +448,36 @@ async function fetchAccountQuota(
|
|
|
449
448
|
catch { return false; }
|
|
450
449
|
};
|
|
451
450
|
const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
|
|
451
|
+
const unavailable = (): AccountQuotaCacheEntry => {
|
|
452
|
+
if (!flightCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
|
|
453
|
+
const previous = accountQuotaCache.get(key);
|
|
454
|
+
const retained = provider === "anthropic" && previous?.isCurrent?.() === false ? undefined : previous;
|
|
455
|
+
const entry: AccountQuotaCacheEntry = {
|
|
456
|
+
ts: Date.now(),
|
|
457
|
+
quota: provider === "anthropic" ? normalizeAnthropicQuota(retained?.quota, Date.now()) : cached?.quota ?? null,
|
|
458
|
+
unavailable: true,
|
|
459
|
+
...(provider === "anthropic" && retained?.isCurrent ? { isCurrent: retained.isCurrent } : {}),
|
|
460
|
+
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
461
|
+
...diagnosticFields(),
|
|
462
|
+
};
|
|
463
|
+
if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
464
|
+
accountQuotaCache.set(key, entry);
|
|
465
|
+
if (provider === "kiro") persistKiroAccountState();
|
|
466
|
+
sweepExpiredOnWrite(entry.ts);
|
|
467
|
+
}
|
|
468
|
+
return entry;
|
|
469
|
+
};
|
|
452
470
|
try {
|
|
453
471
|
if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
|
|
454
472
|
let quota: ProviderQuota | null;
|
|
473
|
+
let anthropicCurrent: (() => boolean) | undefined;
|
|
455
474
|
let kiroSnapshot: KiroUsageSnapshot | null = null;
|
|
456
475
|
if (provider === "kiro") {
|
|
457
476
|
kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
|
|
458
477
|
quota = kiroSnapshot?.quota ?? null;
|
|
459
478
|
} else {
|
|
460
479
|
const token = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
480
|
+
if (!flightCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota flight is stale");
|
|
461
481
|
if (provider === "google-antigravity") {
|
|
462
482
|
// Per-account Gem/Cla windows (#1082). The project id is part of the stored
|
|
463
483
|
// credential; without it the probe cannot be made, and that is "unavailable",
|
|
@@ -469,62 +489,40 @@ async function fetchAccountQuota(
|
|
|
469
489
|
quota = result.kind === "available" ? result.quota : null;
|
|
470
490
|
if (result.kind === "unavailable") quotaFailure = result.failure;
|
|
471
491
|
} else if (provider === "anthropic") {
|
|
472
|
-
|
|
492
|
+
const result = await probeAnthropicQuotaWithRecovery(accountId, token,
|
|
493
|
+
fresh => { assertAnthropicQuotaSendAllowed(accountId, token); return fetchAnthropicUsageQuota(token, fresh); },
|
|
494
|
+
() => mayCommitAccountQuotaKey(key, writerGeneration));
|
|
495
|
+
if (result && !result.isCurrent()) throw new AnthropicQuotaProbeOwnershipError("anthropic quota probe lost publication ownership");
|
|
496
|
+
quota = result?.quota ?? null;
|
|
497
|
+
anthropicCurrent = result?.isCurrent;
|
|
473
498
|
} else {
|
|
474
499
|
return { ts: Date.now(), quota: null, unavailable: true };
|
|
475
500
|
}
|
|
476
501
|
}
|
|
477
|
-
if (!quota)
|
|
478
|
-
// Preserve last-good bars while negative-caching the failed probe.
|
|
479
|
-
const entry: AccountQuotaCacheEntry = {
|
|
480
|
-
ts: Date.now(),
|
|
481
|
-
quota: provider === "anthropic"
|
|
482
|
-
? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
|
|
483
|
-
unavailable: true,
|
|
484
|
-
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
485
|
-
...diagnosticFields(),
|
|
486
|
-
};
|
|
487
|
-
if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
488
|
-
accountQuotaCache.set(key, entry);
|
|
489
|
-
if (provider === "kiro") persistKiroAccountState();
|
|
490
|
-
sweepExpiredOnWrite(entry.ts);
|
|
491
|
-
}
|
|
492
|
-
return entry;
|
|
493
|
-
}
|
|
502
|
+
if (!quota) return unavailable();
|
|
494
503
|
const entry: AccountQuotaCacheEntry = {
|
|
495
504
|
ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
|
|
496
505
|
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
506
|
+
...(anthropicCurrent ? { isCurrent: anthropicCurrent } : {}),
|
|
497
507
|
};
|
|
498
|
-
if (mayCommitAccountQuotaKey(key, writerGeneration)
|
|
508
|
+
if (mayCommitAccountQuotaKey(key, writerGeneration)
|
|
509
|
+
&& (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
499
510
|
accountQuotaCache.set(key, entry);
|
|
500
511
|
if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); }
|
|
501
512
|
sweepExpiredOnWrite(entry.ts);
|
|
502
513
|
}
|
|
503
514
|
return entry;
|
|
504
|
-
} catch {
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (provider === "anthropic" && error instanceof AnthropicQuotaProbeOwnershipError) return { ts: Date.now(), quota: null, unavailable: true };
|
|
505
517
|
if (provider === "google-antigravity") quotaFailure = "account_unavailable";
|
|
506
|
-
|
|
507
|
-
ts: Date.now(),
|
|
508
|
-
quota: provider === "anthropic"
|
|
509
|
-
? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
|
|
510
|
-
unavailable: true,
|
|
511
|
-
...(provider === "kiro" ? { identity: kiroIdentity } : {}),
|
|
512
|
-
...diagnosticFields(),
|
|
513
|
-
};
|
|
514
|
-
if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
|
|
515
|
-
accountQuotaCache.set(key, entry);
|
|
516
|
-
if (provider === "kiro") persistKiroAccountState();
|
|
517
|
-
sweepExpiredOnWrite(entry.ts);
|
|
518
|
-
}
|
|
519
|
-
return entry;
|
|
518
|
+
return unavailable();
|
|
520
519
|
}
|
|
521
520
|
})().finally(() => {
|
|
522
|
-
if (accountQuotaInflight.get(
|
|
521
|
+
if (accountQuotaInflight.get(flightKey) === probe) accountQuotaInflight.delete(flightKey);
|
|
523
522
|
});
|
|
524
|
-
accountQuotaInflight.set(
|
|
523
|
+
accountQuotaInflight.set(flightKey, probe);
|
|
525
524
|
return probe;
|
|
526
525
|
}
|
|
527
|
-
|
|
528
526
|
/**
|
|
529
527
|
* Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
|
|
530
528
|
* single failing account never blocks the others.
|
|
@@ -546,6 +544,7 @@ export async function fetchProviderAccountQuotas(
|
|
|
546
544
|
...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
|
|
547
545
|
};
|
|
548
546
|
if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
|
|
547
|
+
if (provider === "anthropic" && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
|
|
549
548
|
if (!explicitAccountReader(provider)) return result;
|
|
550
549
|
const identity = entry.identity;
|
|
551
550
|
Object.defineProperty(result, "isCurrent", { value: () => {
|
|
@@ -190,7 +190,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
190
190
|
// (it is the current catalog, so its default ordering wins), then the ids
|
|
191
191
|
// only the old devin entry carried. Degraded-mode seed only either way —
|
|
192
192
|
// `liveModels` discovers the account's real roster.
|
|
193
|
-
models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
|
|
193
|
+
models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-1-sol", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5-5", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "grok-4-7", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
|
|
194
194
|
liveModels: true,
|
|
195
195
|
defaultModel: "swe-2",
|
|
196
196
|
modelContextWindows: DEVIN_MODEL_CONTEXT_WINDOWS,
|
|
@@ -1025,6 +1025,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
|
|
|
1025
1025
|
// 260923 preemptive: GPT-6 Sol/Luna are OpenAI-backed routes like the GPT-5.6 rows above.
|
|
1026
1026
|
"openai/gpt-6-sol": true,
|
|
1027
1027
|
"openai/gpt-6-luna": true,
|
|
1028
|
+
// Listed 2026-09-29 as an OpenAI-backed route like GPT-6 Sol.
|
|
1029
|
+
"openai/gpt-6.1-sol": true,
|
|
1028
1030
|
},
|
|
1029
1031
|
// Deliberately no OpenRouter route pin: it bills the endpoint actually used and reports the
|
|
1030
1032
|
// actual service_tier. B0 confirmation therefore owns downgrade safety. Forcing `only` plus
|