@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
package/src/oauth/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
|
47
47
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
48
48
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
49
49
|
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
50
|
-
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
50
|
+
import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
51
51
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
52
52
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
53
53
|
import { logOAuthEvent } from "./log";
|
|
@@ -1228,7 +1228,7 @@ export function buildModelsRequest(
|
|
|
1228
1228
|
return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
|
|
1229
1229
|
}
|
|
1230
1230
|
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1231
|
-
return { url: discoveryUrl(
|
|
1231
|
+
return { url: discoveryUrl(providerModelsUrl(effectiveProvider.baseUrl)), headers };
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
1234
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
2
|
import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProvider } from "../providers/derive";
|
|
3
|
-
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
3
|
+
import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
|
|
@@ -117,7 +117,7 @@ export async function validateApiKey(
|
|
|
117
117
|
providerName,
|
|
118
118
|
configuredProvider,
|
|
119
119
|
provider.baseUrl,
|
|
120
|
-
|
|
120
|
+
providerModelsUrl(provider.baseUrl),
|
|
121
121
|
);
|
|
122
122
|
const res = await fetch(modelsUrl, {
|
|
123
123
|
headers: { Authorization: `Bearer ${key}` },
|
|
@@ -47,9 +47,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
47
47
|
|
|
48
48
|
const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
// (`reasoning.effort`
|
|
52
|
-
// thinking instructions until their native
|
|
50
|
+
// The GPT-5.6 family (sol/terra/luna) and claude-opus-5 send these values through Kiro's verified
|
|
51
|
+
// native effort fields (`reasoning.effort` for the GPT-5.6 models, `output_config.effort` for
|
|
52
|
+
// claude-opus-5). Other models map them to bounded thinking instructions until their native
|
|
53
|
+
// effort support is verified.
|
|
53
54
|
export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
54
55
|
KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
|
|
55
56
|
);
|
package/src/providers/label.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { CODEX_ACCOUNT_LOG_LABEL_RE, oauthAccountLogLabel } from "../codex/account-label";
|
|
1
|
+
import { 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 {
|
|
5
5
|
return provider === "chatgpt" || provider === "openai-multi" ? "openai" : provider;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function usesApiKeyAccount(provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">): boolean {
|
|
9
|
+
return provider.authMode === "key"
|
|
10
|
+
|| (provider.authMode === undefined && !!provider._apiKeyAttempt?.reference);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Key identity comes from the captured selection, before env/keychain resolution. */
|
|
14
|
+
export function stampApiKeyAccountLabel(
|
|
15
|
+
logCtx: { accountLogLabel?: string },
|
|
16
|
+
providerName: string,
|
|
17
|
+
provider: Pick<OcxProviderConfig, "authMode" | "_apiKeyAttempt">,
|
|
18
|
+
): void {
|
|
19
|
+
if (usesApiKeyAccount(provider)) {
|
|
20
|
+
logCtx.accountLogLabel = apiKeyAccountLogLabel(providerName, provider._apiKeyAttempt);
|
|
21
|
+
} else if (KEY_ACCOUNT_LOG_LABEL_RE.test(logCtx.accountLogLabel ?? "")) {
|
|
22
|
+
delete logCtx.accountLogLabel;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export function baseProviderLabel(provider: string): string {
|
|
9
27
|
const canonical = canonicalUsageProviderLabel(provider);
|
|
10
28
|
if (canonical !== provider) return canonical;
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const MODEL_DISCOVERY_MAX_FILTER_VALUES = 256;
|
|
25
25
|
const MODEL_DISCOVERY_MAX_FILTER_STRING_LENGTH = 1_024;
|
|
26
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
27
|
+
const TRAILING_MODELS = /\/models$/;
|
|
26
28
|
|
|
27
29
|
export interface ResolvedProviderModelDiscovery {
|
|
28
30
|
spec?: ProviderModelDiscoverySpec;
|
|
@@ -50,6 +52,20 @@ export type ModelEnvelopeRowsResult =
|
|
|
50
52
|
| { ok: true; rows: unknown[] }
|
|
51
53
|
| { ok: false; reason: "invalid_shape" | "too_many_models" };
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Build the default OpenAI-compatible model-discovery URL from a configured baseUrl.
|
|
57
|
+
*
|
|
58
|
+
* `baseUrl` is required on both `OcxProviderConfig` and the persisted-config schema, so a row
|
|
59
|
+
* without one is not a state configuration loading can produce. It is deliberately not tolerated
|
|
60
|
+
* here: the old template-literal join silently produced `"undefined/models"`, which is not a usable
|
|
61
|
+
* fallback either — it only ever survived because a static row returns before the URL is parsed.
|
|
62
|
+
*/
|
|
63
|
+
export function providerModelsUrl(baseUrl: string): string {
|
|
64
|
+
const trimmed = baseUrl.trim().replace(TRAILING_SLASHES, "");
|
|
65
|
+
const withoutEndpoint = trimmed.replace(TRAILING_MODELS, "");
|
|
66
|
+
return `${withoutEndpoint}/models`;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
function positiveIntegerAtMost(value: number | undefined, hardLimit: number): number {
|
|
54
70
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return hardLimit;
|
|
55
71
|
return Math.min(Math.floor(value), hardLimit);
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getValidAccessTokenForAccount } from "../../oauth";
|
|
3
|
+
import { getAccountCredential, getAccountSet } from "../../oauth/store";
|
|
4
|
+
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
5
|
+
import { ACCOUNT_QUOTA_TTL_MS, toFiniteNumber } from "../quota-wire";
|
|
6
|
+
import { clearKiroAccountUsageState, reconcileKiroAccountUsageState } from "../kiro-usage";
|
|
7
|
+
import { cancelPendingAccountQuotaPersist, readPersistedAccountQuotas, schedulePersistAccountQuotas } from "../account-quota-disk";
|
|
8
|
+
import { replaceCachedProviderQuotas } from "../quota-routing-cache";
|
|
9
|
+
import { getProviderRegistryEntry } from "../registry";
|
|
10
|
+
import { getProviderQuotaReportCache, hasQuotaRows, routingEvidence, setProviderQuotaReportCache } from "./report-cache";
|
|
11
|
+
import { isCanonicalCommandCodeBaseUrl, isCanonicalKimiCodeBaseUrl } from "./vendor-probes-key";
|
|
12
|
+
import type { AccountQuotaMode, ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";
|
|
13
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
14
|
+
|
|
15
|
+
/** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
|
|
16
|
+
const ACCOUNT_TOKEN_SKEW_MS = 60_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Anthropic and Kiro both report usage per CREDENTIAL, so every logged-in account can be
|
|
20
|
+
* probed with its own bearer token — the active-account selection and the local usage log
|
|
21
|
+
* are irrelevant here. Mirrors the Codex pool behaviour
|
|
22
|
+
* (codex/auth-api.ts:fetchPoolAccountQuota), including a per-account TTL so N accounts cost
|
|
23
|
+
* at most N upstream calls per window. `ACCOUNT_QUOTA_TTL_MS` lives in `quota-wire.ts`
|
|
24
|
+
* because the Kiro exhaustion reader applies the same staleness bound.
|
|
25
|
+
*/
|
|
26
|
+
export type AccountQuotaCacheEntry = {
|
|
27
|
+
ts: number;
|
|
28
|
+
quota: ProviderQuota | null;
|
|
29
|
+
/** Last probe failed (429 / network / expired login); still may hold last-good quota. */
|
|
30
|
+
unavailable?: true;
|
|
31
|
+
quotaFailure?: QuotaFailureCode;
|
|
32
|
+
quotaFailureIsCurrent?: () => boolean;
|
|
33
|
+
/** Private new-reader identity; never persisted or serialized. */
|
|
34
|
+
identity?: string;
|
|
35
|
+
isCurrent?: () => boolean;
|
|
36
|
+
};
|
|
37
|
+
/** Expired measurements become unknown; missing reset evidence never implies a fresh allowance. */
|
|
38
|
+
export function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined, now: number): ProviderQuota | null {
|
|
39
|
+
if (!quota) return null;
|
|
40
|
+
const validReset = (resetAt: unknown): resetAt is number => typeof resetAt === "number"
|
|
41
|
+
&& Number.isFinite(resetAt) && resetAt > 0 && Number.isFinite(new Date(resetAt).getTime());
|
|
42
|
+
let result = quota;
|
|
43
|
+
for (const [percent, reset] of [
|
|
44
|
+
["fiveHourPercent", "fiveHourResetAt"],
|
|
45
|
+
["weeklyPercent", "weeklyResetAt"],
|
|
46
|
+
["monthlyPercent", "monthlyResetAt"],
|
|
47
|
+
] as const) {
|
|
48
|
+
const resetAt = quota[reset];
|
|
49
|
+
if (resetAt === undefined) continue;
|
|
50
|
+
const valid = validReset(resetAt);
|
|
51
|
+
if (valid && resetAt > now) continue;
|
|
52
|
+
if (result === quota) result = { ...quota };
|
|
53
|
+
if (valid) delete result[percent];
|
|
54
|
+
delete result[reset];
|
|
55
|
+
}
|
|
56
|
+
// Persisted rows validate only the outer quota object, so custom data may be malformed.
|
|
57
|
+
if (quota.customWindows !== undefined) {
|
|
58
|
+
const windows = Array.isArray(quota.customWindows) ? quota.customWindows : [];
|
|
59
|
+
const retained: ProviderQuotaWindow[] = [];
|
|
60
|
+
let changed = !Array.isArray(quota.customWindows);
|
|
61
|
+
for (const window of windows) {
|
|
62
|
+
if (!window || typeof window !== "object" || typeof window.label !== "string" || !window.label.trim()
|
|
63
|
+
|| typeof window.percent !== "number" || !Number.isFinite(window.percent)
|
|
64
|
+
|| window.percent < 0 || window.percent > 100) {
|
|
65
|
+
changed = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (validReset(window.resetAt) && window.resetAt <= now) {
|
|
69
|
+
changed = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (window.resetAt !== undefined && !validReset(window.resetAt)) {
|
|
73
|
+
const normalized = { ...window };
|
|
74
|
+
delete normalized.resetAt;
|
|
75
|
+
retained.push(normalized);
|
|
76
|
+
changed = true;
|
|
77
|
+
} else {
|
|
78
|
+
retained.push(window);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (changed) {
|
|
82
|
+
if (result === quota) result = { ...quota };
|
|
83
|
+
if (retained.length) result.customWindows = retained;
|
|
84
|
+
else delete result.customWindows;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return hasQuotaRows(result) ? result : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
|
|
91
|
+
export let explicitAccountEpoch = 0;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Seed the cache from the last run, once.
|
|
95
|
+
*
|
|
96
|
+
* Without this a restart forgets every measurement, so the pool opens its next turn with
|
|
97
|
+
* no idea which account has room — the exact blindness pre-dispatch selection exists to
|
|
98
|
+
* remove. A hydrated row is still subject to the ordinary TTL, so it orders the first
|
|
99
|
+
* request and is replaced by a live probe immediately after.
|
|
100
|
+
*/
|
|
101
|
+
let diskHydrated = false;
|
|
102
|
+
export function hydrateAccountQuotaCache(): void {
|
|
103
|
+
if (diskHydrated) return;
|
|
104
|
+
diskHydrated = true;
|
|
105
|
+
for (const [key, quota] of readPersistedAccountQuotas()) {
|
|
106
|
+
// Disk stores observation time, not the Anthropic usage probe's clock.
|
|
107
|
+
if (!accountQuotaCache.has(key)) {
|
|
108
|
+
const anthropic = key.startsWith("anthropic\u0000");
|
|
109
|
+
accountQuotaCache.set(key, {
|
|
110
|
+
ts: anthropic ? 0 : quota.updatedAt,
|
|
111
|
+
quota: anthropic ? normalizeAnthropicQuota(quota, Date.now()) : quota,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function persistAccountQuotaCache(): void {
|
|
118
|
+
schedulePersistAccountQuotas(function* () {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
for (const [key, entry] of accountQuotaCache) {
|
|
121
|
+
const quota = key.startsWith("anthropic\u0000") ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
|
|
122
|
+
if (quota) yield [key, quota] as [string, ProviderQuota];
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export const accountQuotaInflight = new Map<string, Promise<AccountQuotaCacheEntry>>();
|
|
127
|
+
let lastReconciledGeneration = 0;
|
|
128
|
+
let liveAccountQuotaKeys = new Set<string>();
|
|
129
|
+
let liveProviderQuotaKeys = new Set<string>();
|
|
130
|
+
|
|
131
|
+
export function mayCommitAccountQuotaKey(key: string, writerGeneration: number): boolean {
|
|
132
|
+
return writerGeneration >= lastReconciledGeneration || liveAccountQuotaKeys.has(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function mayCommitProviderQuotaKey(key: string, writerGeneration: number): boolean {
|
|
136
|
+
return writerGeneration >= lastReconciledGeneration || liveProviderQuotaKeys.has(key);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ProviderAccountQuota {
|
|
140
|
+
accountId: string;
|
|
141
|
+
quota: ProviderQuota | null;
|
|
142
|
+
/** Set when the probe could not reach upstream (expired login, 429, network). */
|
|
143
|
+
unavailable?: true;
|
|
144
|
+
quotaFailure?: QuotaFailureCode;
|
|
145
|
+
quotaFailureIsCurrent?: () => boolean;
|
|
146
|
+
isCurrent?: () => boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
150
|
+
export function supportsPerAccountQuota(provider: string): boolean {
|
|
151
|
+
return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
|
|
152
|
+
|| explicitAccountReader(provider);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function explicitAccountReader(provider: string): boolean {
|
|
156
|
+
return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
|
|
160
|
+
return hasPassiveAccountQuota(provider) ? "passive" : supportsPerAccountQuota(provider) ? "probe" : "unsupported";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function accountCacheKey(provider: string, accountId: string): string {
|
|
164
|
+
return `${provider}\u0000${accountId}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Synchronous last-good per-account quota read for routing. Never probes the network.
|
|
169
|
+
* Returns null when nothing is cached (or the cached row has no bars).
|
|
170
|
+
*/
|
|
171
|
+
export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
|
|
172
|
+
const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
|
|
173
|
+
if (entry?.isCurrent && !entry.isCurrent()) return null;
|
|
174
|
+
return provider === "anthropic" ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Test-only: seed or clear the per-account quota cache without probing upstream. */
|
|
178
|
+
export function setCachedProviderAccountQuotaForTests(
|
|
179
|
+
provider: string,
|
|
180
|
+
accountId: string,
|
|
181
|
+
quota: ProviderQuota | null,
|
|
182
|
+
): void {
|
|
183
|
+
const key = accountCacheKey(provider, accountId);
|
|
184
|
+
if (quota === null) {
|
|
185
|
+
accountQuotaCache.delete(key);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
accountQuotaCache.set(key, { ts: Date.now(), quota });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Unified headers report utilization fractions and epoch-second reset times. */
|
|
192
|
+
function anthropicHeaderResetAt(value: string | null): number | undefined {
|
|
193
|
+
const seconds = toFiniteNumber(value);
|
|
194
|
+
if (seconds === undefined || seconds <= 0) return undefined;
|
|
195
|
+
const timestamp = seconds * 1000;
|
|
196
|
+
return Number.isFinite(new Date(timestamp).getTime()) ? timestamp : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseAnthropicRateLimitHeaders(headers: Headers): ProviderQuota | null {
|
|
200
|
+
const fiveHourPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-5h-utilization"));
|
|
201
|
+
const weeklyPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-7d-utilization"));
|
|
202
|
+
if (fiveHourPercent === undefined && weeklyPercent === undefined) return null;
|
|
203
|
+
const fiveHourResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-5h-reset"));
|
|
204
|
+
const weeklyResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-7d-reset"));
|
|
205
|
+
return {
|
|
206
|
+
...(fiveHourPercent !== undefined ? { fiveHourPercent } : {}),
|
|
207
|
+
...(fiveHourPercent !== undefined && fiveHourResetAt !== undefined ? { fiveHourResetAt } : {}),
|
|
208
|
+
...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
|
|
209
|
+
...(weeklyPercent !== undefined && weeklyResetAt !== undefined ? { weeklyResetAt } : {}),
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Reject unknown scales; round fraction conversion for persisted/displayed percentages. */
|
|
215
|
+
function normalizeUtilizationFraction(value: string | null): number | undefined {
|
|
216
|
+
const numeric = toFiniteNumber(value);
|
|
217
|
+
if (numeric === undefined || numeric < 0 || numeric > 1) return undefined;
|
|
218
|
+
return Math.round(numeric * 10_000) / 100;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Merge serving-account observations without advancing the usage probe's clock or
|
|
223
|
+
* erasing model-specific windows. The caller owns credential attribution; this guard
|
|
224
|
+
* prevents a retired account key from being revived by an older config generation.
|
|
225
|
+
*/
|
|
226
|
+
export function recordAnthropicAccountQuotaFromHeaders(
|
|
227
|
+
accountId: string,
|
|
228
|
+
headers: Headers,
|
|
229
|
+
writerGeneration: number,
|
|
230
|
+
): void {
|
|
231
|
+
if (!accountId) return;
|
|
232
|
+
const observed = parseAnthropicRateLimitHeaders(headers);
|
|
233
|
+
if (!observed) return;
|
|
234
|
+
const key = accountCacheKey("anthropic", accountId);
|
|
235
|
+
if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
|
|
236
|
+
// Hydrate before writing, for the same reason `recordPassiveAccountQuota` does: this write
|
|
237
|
+
// arrives unprompted from the request path, and `persistAccountQuotaCache` serializes the
|
|
238
|
+
// whole map. Landing before any reader has hydrated would persist this single row and erase
|
|
239
|
+
// every other provider's saved row.
|
|
240
|
+
hydrateAccountQuotaCache();
|
|
241
|
+
const previous = accountQuotaCache.get(key);
|
|
242
|
+
accountQuotaCache.set(key, {
|
|
243
|
+
...previous,
|
|
244
|
+
// Headers do not prove that the last usage probe succeeded.
|
|
245
|
+
ts: previous?.ts ?? 0,
|
|
246
|
+
quota: normalizeAnthropicQuota({
|
|
247
|
+
...normalizeAnthropicQuota(previous?.quota, observed.updatedAt), ...observed,
|
|
248
|
+
}, observed.updatedAt),
|
|
249
|
+
});
|
|
250
|
+
persistAccountQuotaCache();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Providers whose per-account quota is OBSERVED in-band, never probed.
|
|
255
|
+
*
|
|
256
|
+
* Deliberately separate from `supportsPerAccountQuota` rather than folded into it. That
|
|
257
|
+
* predicate gates explicit upstream readers. Meta publishes no quota endpoint, so it
|
|
258
|
+
* remains a cache-only observation even when every probe reader is account-scoped.
|
|
259
|
+
*/
|
|
260
|
+
export function hasPassiveAccountQuota(provider: string): boolean {
|
|
261
|
+
return provider === "meta-muse";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Record a quota observed in-band on a streaming turn.
|
|
266
|
+
*
|
|
267
|
+
* The CALLER captures `writerGeneration` when it resolves the serving credential, not
|
|
268
|
+
* this function at write time. A streaming turn is a long await, and a generation
|
|
269
|
+
* captured immediately before the write cannot see a config or account change that
|
|
270
|
+
* happened EARLIER in the same turn — which is exactly the case the fence exists for.
|
|
271
|
+
*/
|
|
272
|
+
export function recordPassiveAccountQuota(
|
|
273
|
+
provider: string,
|
|
274
|
+
accountId: string,
|
|
275
|
+
quota: ProviderQuota,
|
|
276
|
+
writerGeneration: number,
|
|
277
|
+
): void {
|
|
278
|
+
if (!hasPassiveAccountQuota(provider) || !accountId) return;
|
|
279
|
+
const key = accountCacheKey(provider, accountId);
|
|
280
|
+
if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
|
|
281
|
+
// Hydrate BEFORE writing, not only on the read path. `persistAccountQuotaCache`
|
|
282
|
+
// serializes the whole in-memory map, so a passive write that lands before anything
|
|
283
|
+
// has read the cache would persist this one row and erase every other provider's
|
|
284
|
+
// saved row -- and `diskHydrated` would then stop any later reader from recovering
|
|
285
|
+
// them. A probe writer cannot hit this because its own read hydrates first; an
|
|
286
|
+
// observation arrives unprompted, so it must hydrate itself.
|
|
287
|
+
hydrateAccountQuotaCache();
|
|
288
|
+
accountQuotaCache.set(key, { ts: Date.now(), quota });
|
|
289
|
+
// Persisted so a restart keeps the last observation: with no probe to re-establish it,
|
|
290
|
+
// a forgotten row stays forgotten until the user happens to run another streaming turn.
|
|
291
|
+
persistAccountQuotaCache();
|
|
292
|
+
// sweepExpiredOnWrite is deliberately NOT called. Existing probe writers call it
|
|
293
|
+
// because they run on a poll; this runs on the request path, where a state sweep does
|
|
294
|
+
// not belong. Passive rows are still reclaimed by generation reconciliation
|
|
295
|
+
// (reconcileProviderAccountQuotaRows) and by the disk reader's age bound.
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Cache-only per-account rows for a passive provider. Never probes, never refreshes.
|
|
300
|
+
*
|
|
301
|
+
* An account with no observation is OMITTED rather than returned with `quota: null` and
|
|
302
|
+
* `unavailable`: that pair means "a probe was attempted and failed", and no probe was
|
|
303
|
+
* ever attempted here. A user who has not yet run a streaming turn simply has no
|
|
304
|
+
* measurement, which is not an error state.
|
|
305
|
+
*/
|
|
306
|
+
export function readPassiveProviderAccountQuotas(provider: string): ProviderAccountQuota[] {
|
|
307
|
+
if (!hasPassiveAccountQuota(provider)) return [];
|
|
308
|
+
// Idempotent, and otherwise only reached from probe paths a passive provider never
|
|
309
|
+
// enters — without it a restart shows nothing until the next streaming turn, even
|
|
310
|
+
// though the row is sitting on disk.
|
|
311
|
+
hydrateAccountQuotaCache();
|
|
312
|
+
const set = getAccountSet(provider);
|
|
313
|
+
if (!set) return [];
|
|
314
|
+
const rows: ProviderAccountQuota[] = [];
|
|
315
|
+
for (const account of set.accounts) {
|
|
316
|
+
const entry = accountQuotaCache.get(accountCacheKey(provider, account.id));
|
|
317
|
+
if (entry?.quota) rows.push({ accountId: account.id, quota: entry.quota });
|
|
318
|
+
}
|
|
319
|
+
return rows;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
|
|
323
|
+
let removed = 0;
|
|
324
|
+
for (const [key, entry] of accountQuotaCache) {
|
|
325
|
+
// Anthropic observations extend retention, never the usage probe's eligibility clock.
|
|
326
|
+
const retainedAt = key.startsWith("anthropic\u0000")
|
|
327
|
+
? Math.max(entry.ts, entry.quota?.updatedAt ?? 0)
|
|
328
|
+
: entry.ts;
|
|
329
|
+
if (retainedAt + ACCOUNT_QUOTA_TTL_MS > now) continue;
|
|
330
|
+
accountQuotaCache.delete(key);
|
|
331
|
+
removed += 1;
|
|
332
|
+
}
|
|
333
|
+
return removed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function reconcileProviderAccountQuotaRows(context: GenerationContext): number {
|
|
337
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
338
|
+
let removed = 0;
|
|
339
|
+
for (const key of accountQuotaCache.keys()) {
|
|
340
|
+
if (context.oauthAccountKeys.has(key)) continue;
|
|
341
|
+
accountQuotaCache.delete(key);
|
|
342
|
+
removed += 1;
|
|
343
|
+
}
|
|
344
|
+
// Kiro exhaustion rows are keyed identically, so they retire with their quota row; a
|
|
345
|
+
// verdict outliving its account would hand the replacement a cooldown it never earned.
|
|
346
|
+
removed += reconcileKiroAccountUsageState(context.oauthAccountKeys);
|
|
347
|
+
const cachedReports = getProviderQuotaReportCache();
|
|
348
|
+
if (cachedReports) {
|
|
349
|
+
const reports = cachedReports.response.reports.filter(report => context.providerNames.has(report.provider));
|
|
350
|
+
removed += cachedReports.response.reports.length - reports.length;
|
|
351
|
+
setProviderQuotaReportCache({ ...cachedReports, response: { ...cachedReports.response, reports } });
|
|
352
|
+
replaceCachedProviderQuotas(reports, routingEvidence);
|
|
353
|
+
}
|
|
354
|
+
liveAccountQuotaKeys = new Set(context.oauthAccountKeys);
|
|
355
|
+
liveProviderQuotaKeys = new Set(context.providerNames);
|
|
356
|
+
lastReconciledGeneration = context.generation;
|
|
357
|
+
return removed;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Test-only reset so a direct reconcile call in one file cannot leak across files. */
|
|
361
|
+
export function resetProviderQuotaReconcileStateForTests(): void {
|
|
362
|
+
lastReconciledGeneration = 0;
|
|
363
|
+
liveAccountQuotaKeys = new Set();
|
|
364
|
+
liveProviderQuotaKeys = new Set();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Drop cached per-account rows (all, or just one provider's). */
|
|
368
|
+
export function clearAccountQuotaCache(provider?: string): void {
|
|
369
|
+
explicitAccountEpoch += 1;
|
|
370
|
+
if (!provider) {
|
|
371
|
+
accountQuotaCache.clear();
|
|
372
|
+
accountQuotaInflight.clear();
|
|
373
|
+
clearKiroAccountUsageState();
|
|
374
|
+
// A cleared cache must not be re-seeded from the file it was just cleared of, and any
|
|
375
|
+
// pending write of the old rows is abandoned.
|
|
376
|
+
diskHydrated = false;
|
|
377
|
+
cancelPendingAccountQuotaPersist();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const prefix = `${provider}\u0000`;
|
|
381
|
+
for (const key of [...accountQuotaCache.keys()]) {
|
|
382
|
+
if (key.startsWith(prefix)) accountQuotaCache.delete(key);
|
|
383
|
+
}
|
|
384
|
+
clearKiroAccountUsageState(prefix);
|
|
385
|
+
// Drop in-flight probes too so a late resolve cannot repopulate after logout/remove.
|
|
386
|
+
for (const key of [...accountQuotaInflight.keys()]) {
|
|
387
|
+
if (key.startsWith(prefix)) accountQuotaInflight.delete(key);
|
|
388
|
+
}
|
|
389
|
+
persistAccountQuotaCache();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Resolve a bearer for quota probing without silently adopting a newer global
|
|
394
|
+
* Claude CLI credential into a background multiauth slot.
|
|
395
|
+
*
|
|
396
|
+
* - Fresh stored access → use as-is (no refresh).
|
|
397
|
+
* - Active account with expired access → normal refresh path.
|
|
398
|
+
* - Background `local-cli` with expired access → fail closed (unavailable):
|
|
399
|
+
* `getValidAccessTokenForAccount` can persist a mismatched Claude CLI identity.
|
|
400
|
+
* - Background ordinary OAuth (`source !== "local-cli"`) → safe to refresh;
|
|
401
|
+
* Anthropic's lock only adopts disk credentials for `local-cli` rows.
|
|
402
|
+
*/
|
|
403
|
+
export async function getTokenForAccountQuotaProbe(provider: string, accountId: string): Promise<string> {
|
|
404
|
+
const stored = getAccountCredential(provider, accountId);
|
|
405
|
+
if (!stored) throw new Error("account credential missing");
|
|
406
|
+
if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
|
|
407
|
+
const activeId = getAccountSet(provider)?.activeAccountId;
|
|
408
|
+
if (activeId !== accountId && stored.source === "local-cli") {
|
|
409
|
+
throw new Error("background local-cli token expired; skip CLI-adopting refresh for quota probe");
|
|
410
|
+
}
|
|
411
|
+
return getValidAccessTokenForAccount(provider, accountId);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
|
|
415
|
+
if (configured) return configured;
|
|
416
|
+
const entry = getProviderRegistryEntry(provider);
|
|
417
|
+
return entry ? { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: "oauth" } : undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function explicitQuotaIdentity(provider: string, accountId: string, configured?: OcxProviderConfig): string | undefined {
|
|
421
|
+
const credential = getAccountCredential(provider, accountId);
|
|
422
|
+
const target = explicitQuotaConfig(provider, configured);
|
|
423
|
+
if (!credential || !target) return undefined;
|
|
424
|
+
return quotaCredentialIdentity(provider, accountId, credential, target);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function quotaCredentialIdentity(provider: string, accountId: string, credential: NonNullable<ReturnType<typeof getAccountCredential>>, target: OcxProviderConfig): string {
|
|
428
|
+
return createHash("sha256").update(JSON.stringify([
|
|
429
|
+
provider, accountId, credential.access, credential.refresh, credential.expires,
|
|
430
|
+
credential.accountId, credential.projectId, credential.source,
|
|
431
|
+
target.adapter, target.baseUrl, target.authMode, target.disabled === true,
|
|
432
|
+
])).digest("hex");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function explicitQuotaDestination(provider: string, config: OcxProviderConfig): boolean {
|
|
436
|
+
if (config.disabled === true || config.authMode !== "oauth") return false;
|
|
437
|
+
if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl);
|
|
438
|
+
if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
|
|
439
|
+
// These readers use fixed canonical billing origins, never config.baseUrl.
|
|
440
|
+
return provider === "xai" || provider === "cursor";
|
|
441
|
+
}
|