@bitkyc08/opencodex 2.76.0 → 2.77.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-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/oauth/health.ts
CHANGED
|
@@ -14,6 +14,22 @@ import { loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshI
|
|
|
14
14
|
import type { ProviderAccount } from "./types";
|
|
15
15
|
|
|
16
16
|
export type OAuthAccountHealth =
|
|
17
|
+
| { status: "healthy" }
|
|
18
|
+
| { status: "cooldown"; until: string; reason: "rate_limit" | "quota" }
|
|
19
|
+
| { status: "reauth_required"; reason: OAuthReauthReason }
|
|
20
|
+
| { status: "warning"; reason: "refresh_conflict" | "metadata_mismatch" | "stale_credentials" | "validation_pending" };
|
|
21
|
+
|
|
22
|
+
/** Why an OAuth account needs reauthentication. `verify_account` is distinct from a dead
|
|
23
|
+
* credential: the grant is alive but the provider blocks the account until a human
|
|
24
|
+
* verifies it, so a plain re-login without that verification will not help. */
|
|
25
|
+
export type OAuthReauthReason = "unauthorized" | "forbidden" | "refresh_failed" | "verify_account";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Codex-pool health: the same shape minus the provider-verification cause, which only
|
|
29
|
+
* the generic OAuth quarantine produces. Narrowing the return keeps Codex DTOs on
|
|
30
|
+
* `CodexAccountReauthReason` without a lossy re-narrow at every consumer.
|
|
31
|
+
*/
|
|
32
|
+
export type CodexPoolAccountHealth =
|
|
17
33
|
| { status: "healthy" }
|
|
18
34
|
| { status: "cooldown"; until: string; reason: "rate_limit" | "quota" }
|
|
19
35
|
| { status: "reauth_required"; reason: "unauthorized" | "forbidden" | "refresh_failed" }
|
|
@@ -24,6 +40,7 @@ export type OAuthHealthLabel =
|
|
|
24
40
|
| "Rate limited"
|
|
25
41
|
| "Quota limited"
|
|
26
42
|
| "Reauthentication required"
|
|
43
|
+
| "Verification required"
|
|
27
44
|
| "Refresh failed"
|
|
28
45
|
| "Metadata mismatch"
|
|
29
46
|
| "Credential conflict"
|
|
@@ -60,7 +77,7 @@ type OAuthWarningReason = "refresh_conflict" | "metadata_mismatch" | "stale_cred
|
|
|
60
77
|
|
|
61
78
|
export function projectOAuthAccountHealth(input: {
|
|
62
79
|
needsReauth?: boolean;
|
|
63
|
-
reauthReason?:
|
|
80
|
+
reauthReason?: OAuthReauthReason;
|
|
64
81
|
cooldownUntilMs?: number;
|
|
65
82
|
cooldownReason?: "rate_limit" | "quota";
|
|
66
83
|
warningReason?: OAuthWarningReason;
|
|
@@ -96,6 +113,9 @@ function actionFor(provider: string, health: OAuthAccountHealth): string | undef
|
|
|
96
113
|
}
|
|
97
114
|
if (health.status === "reauth_required") {
|
|
98
115
|
if (provider === "codex") return CODEX_REAUTH_ACTION;
|
|
116
|
+
if (health.reason === "verify_account") {
|
|
117
|
+
return `verify the account with the provider in a browser, then run \`ocx login ${provider}\``;
|
|
118
|
+
}
|
|
99
119
|
return `run \`ocx login ${provider}\``;
|
|
100
120
|
}
|
|
101
121
|
if (health.status === "cooldown") {
|
|
@@ -115,7 +135,9 @@ export function oauthHealthLabel(health: OAuthAccountHealth): OAuthHealthLabel {
|
|
|
115
135
|
case "cooldown":
|
|
116
136
|
return health.reason === "rate_limit" ? "Rate limited" : "Quota limited";
|
|
117
137
|
case "reauth_required":
|
|
118
|
-
|
|
138
|
+
if (health.reason === "refresh_failed") return "Refresh failed";
|
|
139
|
+
if (health.reason === "verify_account") return "Verification required";
|
|
140
|
+
return "Reauthentication required";
|
|
119
141
|
case "warning":
|
|
120
142
|
switch (health.reason) {
|
|
121
143
|
case "validation_pending":
|
|
@@ -190,7 +212,9 @@ export function projectStoredOAuthAccountHealth(
|
|
|
190
212
|
: null;
|
|
191
213
|
return projectOAuthAccountHealth({
|
|
192
214
|
needsReauth: account.needsReauth === true,
|
|
193
|
-
reauthReason: account.needsReauth === true
|
|
215
|
+
reauthReason: account.needsReauth === true
|
|
216
|
+
? (account.needsReauthReason ?? "refresh_failed")
|
|
217
|
+
: undefined,
|
|
194
218
|
cooldownUntilMs: anthropicSnap?.cooldownUntil,
|
|
195
219
|
// Same mapping as the Codex pool's `cooldownReasonFromSource`: only a Retry-After is
|
|
196
220
|
// request-rate throttling. A reset-derived cooldown means a usage window is spent, which
|
|
@@ -206,7 +230,7 @@ export function projectCodexAccountHealth(input: {
|
|
|
206
230
|
needsReauth: boolean;
|
|
207
231
|
reauthReason?: "unauthorized" | "forbidden" | "refresh_failed";
|
|
208
232
|
now?: number;
|
|
209
|
-
}):
|
|
233
|
+
}): CodexPoolAccountHealth {
|
|
210
234
|
// One read serves every verdict below. Each lookup re-reads and re-hardens the whole store
|
|
211
235
|
// file, and the main account lives in the native Codex auth file rather than the pool store,
|
|
212
236
|
// so a lookup for it could only ever miss.
|
|
@@ -250,13 +274,16 @@ export function projectCodexAccountHealth(input: {
|
|
|
250
274
|
}
|
|
251
275
|
const now = input.now ?? Date.now();
|
|
252
276
|
const snap = getCodexAccountHealthSnapshot(input.accountId, now);
|
|
277
|
+
// Safe narrowing: every reason above is Codex-scoped (`verify_account` only enters
|
|
278
|
+
// through the generic OAuth quarantine path), so the shared projector cannot
|
|
279
|
+
// produce it here despite the wider return type.
|
|
253
280
|
return projectOAuthAccountHealth({
|
|
254
281
|
needsReauth,
|
|
255
282
|
reauthReason: needsReauth ? (input.reauthReason ?? storedFailureReason ?? "refresh_failed") : undefined,
|
|
256
283
|
cooldownUntilMs: snap?.cooldownUntil,
|
|
257
284
|
cooldownReason: cooldownReasonFromSource(snap?.cooldownSource),
|
|
258
285
|
now,
|
|
259
|
-
});
|
|
286
|
+
}) as CodexPoolAccountHealth;
|
|
260
287
|
}
|
|
261
288
|
|
|
262
289
|
/**
|
package/src/oauth/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
|
52
52
|
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
53
53
|
import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
54
54
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
55
|
-
import {
|
|
55
|
+
import { detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
56
56
|
import { logOAuthEvent } from "./log";
|
|
57
57
|
import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
58
58
|
import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode, type OAuthLoginHint } from "./login-flow-state";
|
|
@@ -82,7 +82,9 @@ export {
|
|
|
82
82
|
export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
|
|
83
83
|
import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
86
|
+
import { newerClaudeCredential, captureAnthropicCredentialOwner } from "./anthropic-continuity";
|
|
87
|
+
import { mergeAnthropicIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
|
|
86
88
|
export interface OAuthAccessSnapshot {
|
|
87
89
|
provider: string;
|
|
88
90
|
accountId: string;
|
|
@@ -169,7 +171,7 @@ export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string;
|
|
|
169
171
|
const XAI_PERMANENT_FAILURE_TTL_MS=30_000;
|
|
170
172
|
const permanentRefreshFailures=new Map<string,number>();
|
|
171
173
|
interface XaiRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
|
|
172
|
-
interface AnthropicRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
|
|
174
|
+
interface AnthropicRefreshDeps { resolveIdentity?: AnthropicIdentityResolver; intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
|
|
173
175
|
interface GenericRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
|
|
174
176
|
function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${credentialGeneration(c)}`;}
|
|
175
177
|
function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
|
|
@@ -866,6 +868,7 @@ function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OA
|
|
|
866
868
|
function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
|
|
867
869
|
return {
|
|
868
870
|
...fresh,
|
|
871
|
+
...mergeAnthropicIdentity(fresh, previous),
|
|
869
872
|
// Shared: a refresh function returns "local-cli" only when the credential it hands back
|
|
870
873
|
// still is the local CLI's (Devin re-reading the CLI file, Meta Muse echoing its durable
|
|
871
874
|
// CLI key). Relabelling that "oauth" would stop the next forced refresh from re-reading it.
|
|
@@ -880,12 +883,6 @@ function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCrede
|
|
|
880
883
|
}
|
|
881
884
|
export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const row=getAccountCredentialWithStatus(provider,accountId);if(!row)throw new OAuthLoginRequiredError(provider);if(row.paused)throw new OAuthAccountPausedError();const stored=row.credential;const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
|
|
882
885
|
|
|
883
|
-
function newerClaudeCredential(stored: OAuthCredentials, now: number): OAuthCredentials | undefined {
|
|
884
|
-
if (stored.source !== "local-cli") return undefined;
|
|
885
|
-
const disk = detectClaudeCodeToken();
|
|
886
|
-
if (!disk || disk.expires <= now + REFRESH_SKEW_MS) return undefined;
|
|
887
|
-
return credentialGeneration(disk) !== credentialGeneration(stored) ? disk : undefined;
|
|
888
|
-
}
|
|
889
886
|
|
|
890
887
|
/**
|
|
891
888
|
* Preserve an already-rotated Nous refresh token (RT-B) after a terminal refresh
|
|
@@ -939,33 +936,42 @@ export async function refreshAnthropicAccountWithLock(
|
|
|
939
936
|
const now = deps.now ?? Date.now;
|
|
940
937
|
const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
|
|
941
938
|
try {
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
if (
|
|
945
|
-
|
|
946
|
-
const
|
|
939
|
+
const accountSet = getAccountSet(provider);
|
|
940
|
+
const account = accountSet?.accounts.find(candidate => candidate.id === accountId);
|
|
941
|
+
if (!accountSet || !account) throw new OAuthLoginRequiredError(provider);
|
|
942
|
+
if (account.paused) throw new OAuthAccountPausedError();
|
|
943
|
+
const stored = account.credential;
|
|
944
|
+
const owns = captureAnthropicCredentialOwner(accountSet, accountId);
|
|
947
945
|
const generation = credentialGeneration(stored);
|
|
948
946
|
let pendingIntent = readOAuthRefreshIntent(provider, accountId);
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
947
|
+
const observed = await newerClaudeCredential(stored, now(), deps.signal, deps.resolveIdentity);
|
|
948
|
+
const assertOwner = (store: AuthStore, checkDisk = true) => {
|
|
949
|
+
if (observed.kind !== "absent" && deps.signal?.aborted) throw new OAuthTokenRefreshStaleError();
|
|
950
|
+
if (!owns(store, checkDisk && observed.kind !== "absent" ? observed.diskGeneration : undefined)) {
|
|
951
|
+
if (store[provider]?.accounts.find(a => a.id === accountId)?.paused) throw new OAuthAccountPausedError();
|
|
952
|
+
throw new OAuthTokenRefreshStaleError();
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
assertOwner({ [provider]: getAccountSet(provider)! }, observed.kind !== "adopt");
|
|
956
|
+
if (observed.kind === "adopt") {
|
|
957
|
+
const outcome = await mergeAccountCredential(provider, accountId, observed.credential, {
|
|
958
|
+
expectedGeneration: generation,
|
|
953
959
|
afterPrePersistRead: deps.afterPrePersistRead,
|
|
960
|
+
assertOwnership: assertOwner,
|
|
954
961
|
});
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// failure must not mask a committed credential by throwing over the return below.
|
|
958
|
-
if (pendingIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, pendingIntent);
|
|
959
|
-
if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
|
|
960
|
-
throw new OAuthLoginRequiredError(provider);
|
|
961
|
-
}
|
|
962
|
+
// A superseding write did not adopt our candidate. Its pending intent remains owned by it.
|
|
963
|
+
if (outcome.superseded) throw new OAuthTokenRefreshStaleError();
|
|
962
964
|
if (pendingIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, pendingIntent);
|
|
963
|
-
return
|
|
965
|
+
return observed.credential.access;
|
|
964
966
|
}
|
|
967
|
+
// Unknown rotation may have consumed this refresh token. Refuse without replay or changing health.
|
|
968
|
+
if (observed.kind === "unresolved") throw new OAuthLoginRequiredError(provider);
|
|
965
969
|
if (pendingIntent?.cleanupPending && pendingIntent.generation === generation) {
|
|
966
970
|
resumeAnthropicRefreshIntentCleanup(provider, accountId, pendingIntent);
|
|
967
971
|
pendingIntent = undefined;
|
|
968
972
|
}
|
|
973
|
+
if (observed.kind === "different"
|
|
974
|
+
&& (pendingIntent?.uncertain || pendingIntent?.generation === generation)) throw new OAuthLoginRequiredError(provider);
|
|
969
975
|
if (!pendingIntent?.uncertain && pendingIntent?.generation === generation) {
|
|
970
976
|
if (pendingIntent.staleOwner) throw new OAuthTokenRefreshStaleError();
|
|
971
977
|
if (deps.replacedStaleFlight && pendingIntent.flightId === deps.replacedStaleFlight.flightId) {
|
|
@@ -1007,6 +1013,7 @@ export async function refreshAnthropicAccountWithLock(
|
|
|
1007
1013
|
const outcome = await mergeAccountCredential(provider, accountId, fresh, {
|
|
1008
1014
|
expectedGeneration: generation,
|
|
1009
1015
|
afterPrePersistRead: deps.afterPrePersistRead,
|
|
1016
|
+
assertOwnership: store => { if (!owns(store, undefined, true)) throw new OAuthTokenRefreshStaleError(); },
|
|
1010
1017
|
});
|
|
1011
1018
|
if (outcome.superseded) {
|
|
1012
1019
|
if (attemptIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, attemptIntent);
|
|
@@ -1717,7 +1724,7 @@ export async function runLogin(
|
|
|
1717
1724
|
});
|
|
1718
1725
|
} else {
|
|
1719
1726
|
const saveOptions = {
|
|
1720
|
-
preserveIdentityless: opts?.forceLogin === true,
|
|
1727
|
+
preserveIdentityless: opts?.forceLogin === true || (provider === "anthropic" && cred.source === "local-cli"),
|
|
1721
1728
|
assertBeforePersist: deps.assertCurrentOwner,
|
|
1722
1729
|
};
|
|
1723
1730
|
if (shouldRollbackKiroAccounts && !deps.saveCredential) {
|
|
@@ -1799,6 +1806,7 @@ export interface OAuthAccountSummary {
|
|
|
1799
1806
|
email?: string;
|
|
1800
1807
|
active: boolean;
|
|
1801
1808
|
needsReauth?: boolean;
|
|
1809
|
+
needsReauthReason?: "verify_account";
|
|
1802
1810
|
expiresAt?: number;
|
|
1803
1811
|
/**
|
|
1804
1812
|
* Subscription tier, mirroring the field the OpenAI/Codex provider reports, so a consumer
|
|
@@ -1835,6 +1843,7 @@ export function getLoginStatus(provider: string, maskEmails = true): { loggedIn:
|
|
|
1835
1843
|
email: projectEmail(a.credential.email, maskEmails) ?? undefined,
|
|
1836
1844
|
active: a.id === set.activeAccountId,
|
|
1837
1845
|
...(a.needsReauth ? { needsReauth: true } : {}),
|
|
1846
|
+
...(a.needsReauth && a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
|
|
1838
1847
|
expiresAt: a.credential.expires,
|
|
1839
1848
|
// Explicitly null rather than omitted — see OAuthAccountSummary.plan. No OAuth provider
|
|
1840
1849
|
// exposes a subscription tier today, so there is nothing truthful to put here; deriving one
|
|
@@ -128,3 +128,10 @@ export function detectClaudeCodeToken(): OAuthCredentials | null {
|
|
|
128
128
|
if (!raw) return null;
|
|
129
129
|
return parseClaudeOauthPayload(raw);
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
/** Shared-token continuity for opaque Claude tokens; fully rotated pairs need authenticated account proof. */
|
|
133
|
+
export function hasClaudeCredentialContinuity(stored: OAuthCredentials, disk: OAuthCredentials): boolean {
|
|
134
|
+
const sameToken = (left: string, right: string): boolean =>
|
|
135
|
+
typeof left === "string" && left.trim().length > 0 && left === right;
|
|
136
|
+
return sameToken(stored.refresh, disk.refresh) || sameToken(stored.access, disk.access);
|
|
137
|
+
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* distinct one. Kimi extracts JWT `user_id`/`sub` as accountId; Cursor extracts JWT `sub` —
|
|
20
20
|
* both append distinct identified accounts under multiauth.
|
|
21
21
|
*/
|
|
22
|
+
import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
|
|
22
23
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
24
|
import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
24
25
|
import { join } from "node:path";
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
} from "../lib/state-store-sweeper";
|
|
35
36
|
import { validateCopilotApiBaseUrl } from "./github-copilot";
|
|
36
37
|
import { validateDevinApiBaseUrl } from "./devin/api-base";
|
|
38
|
+
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
37
39
|
import { parseAnthropicAccountThreshold } from "./anthropic-account-threshold";
|
|
38
40
|
import type { OAuthAccountSelection, OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
|
|
39
41
|
|
|
@@ -531,6 +533,8 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
|
|
|
531
533
|
};
|
|
532
534
|
if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
|
|
533
535
|
if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
|
|
536
|
+
const identity = normalizeAnthropicIdentity(candidate.anthropicIdentity, candidate.access);
|
|
537
|
+
if (identity) normalized.anthropicIdentity = identity;
|
|
534
538
|
if (isCredentialSource(candidate.source)) normalized.source = candidate.source;
|
|
535
539
|
if (typeof candidate.projectId === "string" && candidate.projectId.length > 0) normalized.projectId = candidate.projectId;
|
|
536
540
|
if (typeof candidate.apiBaseUrl === "string" && candidate.apiBaseUrl.length > 0) {
|
|
@@ -629,6 +633,9 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
|
|
|
629
633
|
if (candidate.paused === true) account.paused = true;
|
|
630
634
|
const threshold = parseAnthropicAccountThreshold(candidate.autoSwitchThresholdOverride);
|
|
631
635
|
if (threshold !== null) account.autoSwitchThresholdOverride = threshold;
|
|
636
|
+
if (candidate.needsReauth === true && candidate.needsReauthReason === "verify_account") {
|
|
637
|
+
account.needsReauthReason = "verify_account";
|
|
638
|
+
}
|
|
632
639
|
if (typeof candidate.addedAt === "number") account.addedAt = candidate.addedAt;
|
|
633
640
|
if (typeof candidate.loginId === "string"
|
|
634
641
|
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate.loginId)) {
|
|
@@ -862,9 +869,10 @@ export async function saveCredentialWithReceipt(
|
|
|
862
869
|
): Promise<OAuthCredentialWriteReceipt | null> {
|
|
863
870
|
const safe = normalizeCredential(cred);
|
|
864
871
|
if (!safe) return null;
|
|
865
|
-
|
|
872
|
+
const receipt = await mutateStore(store => {
|
|
866
873
|
const set = store[provider];
|
|
867
874
|
const previousActiveAccountId = set?.activeAccountId;
|
|
875
|
+
const previousSelectionRevision = set?.selectionRevision;
|
|
868
876
|
const previousAccounts = new Map(
|
|
869
877
|
set?.accounts.map(account => [account.id, structuredClone(account)]) ?? [],
|
|
870
878
|
);
|
|
@@ -876,11 +884,27 @@ export async function saveCredentialWithReceipt(
|
|
|
876
884
|
const id = newAccountId(safe);
|
|
877
885
|
store[provider] = { activeAccountId: id, accounts: [{ id, credential: safe, addedAt: Date.now() }] };
|
|
878
886
|
accountId = id;
|
|
887
|
+
} else if (provider === "anthropic" && safe.source === "local-cli") {
|
|
888
|
+
const existing = set.accounts.find(a => matchesAnthropicImport(a.credential, safe));
|
|
889
|
+
if (existing) {
|
|
890
|
+
existing.credential = { ...safe, ...mergeAnthropicIdentity(safe, existing.credential),
|
|
891
|
+
accountId: safe.accountId ?? existing.credential.accountId, email: safe.email ?? existing.credential.email };
|
|
892
|
+
set.selectionRevision = previousSelectionRevision;
|
|
893
|
+
delete existing.needsReauth;
|
|
894
|
+
delete existing.needsReauthReason;
|
|
895
|
+
accountId = existing.id;
|
|
896
|
+
} else {
|
|
897
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
898
|
+
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
899
|
+
set.activeAccountId = id;
|
|
900
|
+
accountId = id;
|
|
901
|
+
}
|
|
879
902
|
} else if (identity) {
|
|
880
903
|
const existing = set.accounts.find(a => (a.credential.accountId ?? a.credential.email) === identity);
|
|
881
904
|
if (existing) {
|
|
882
905
|
existing.credential = safe;
|
|
883
906
|
delete existing.needsReauth;
|
|
907
|
+
delete existing.needsReauthReason;
|
|
884
908
|
if (existing.paused !== true) set.activeAccountId = existing.id;
|
|
885
909
|
accountId = existing.id;
|
|
886
910
|
} else {
|
|
@@ -892,6 +916,7 @@ export async function saveCredentialWithReceipt(
|
|
|
892
916
|
if (!opts.preserveIdentityless && active && active.credential.accountId === undefined && active.credential.email === undefined) {
|
|
893
917
|
active.credential = safe;
|
|
894
918
|
delete active.needsReauth;
|
|
919
|
+
delete active.needsReauthReason;
|
|
895
920
|
accountId = active.id;
|
|
896
921
|
} else {
|
|
897
922
|
const id = distinctAccountId(safe, set.accounts);
|
|
@@ -911,6 +936,7 @@ export async function saveCredentialWithReceipt(
|
|
|
911
936
|
if (active) {
|
|
912
937
|
active.credential = safe;
|
|
913
938
|
delete active.needsReauth;
|
|
939
|
+
delete active.needsReauthReason;
|
|
914
940
|
accountId = active.id;
|
|
915
941
|
} else {
|
|
916
942
|
const id = distinctAccountId(safe, set.accounts);
|
|
@@ -944,6 +970,18 @@ export async function saveCredentialWithReceipt(
|
|
|
944
970
|
receipt.selectionRevision = store[provider]?.selectionRevision;
|
|
945
971
|
},
|
|
946
972
|
});
|
|
973
|
+
// An explicit login retires that account's failover evidence: a cooldown recorded
|
|
974
|
+
// against the old grant must not hold out the fresh credential. Dynamic import
|
|
975
|
+
// keeps the store free of a static edge into the failover module, which reads the
|
|
976
|
+
// store itself.
|
|
977
|
+
try {
|
|
978
|
+
const { clearGenericFailoverHealthForAccount } = await import("./generic-account-failover");
|
|
979
|
+
clearGenericFailoverHealthForAccount(provider, receipt.accountId);
|
|
980
|
+
} catch {
|
|
981
|
+
// The credential write has already committed. A cleanup failure must not
|
|
982
|
+
// surface as a failed login, or the Kiro path rolls the new grant back.
|
|
983
|
+
}
|
|
984
|
+
return receipt;
|
|
947
985
|
}
|
|
948
986
|
|
|
949
987
|
/** Ordinary callers do not acquire rollback authority merely by saving a credential. */
|
|
@@ -1047,6 +1085,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1047
1085
|
if (existing && set) {
|
|
1048
1086
|
existing.credential = safe;
|
|
1049
1087
|
delete existing.needsReauth;
|
|
1088
|
+
delete existing.needsReauthReason;
|
|
1050
1089
|
set.activeAccountId ??= existing.id;
|
|
1051
1090
|
return "updated";
|
|
1052
1091
|
}
|
|
@@ -1149,6 +1188,7 @@ export async function saveAccountCredential(
|
|
|
1149
1188
|
account.credential = safe;
|
|
1150
1189
|
if (opts.rotateLoginId) account.loginId = randomUUID();
|
|
1151
1190
|
delete account.needsReauth;
|
|
1191
|
+
delete account.needsReauthReason;
|
|
1152
1192
|
// A pause that found no usable fallback leaves the active id on a paused row. Once this
|
|
1153
1193
|
// reauthentication makes an unpaused account usable, hand the selection to it.
|
|
1154
1194
|
const active = set.accounts.find(a => a.id === set.activeAccountId);
|
|
@@ -1288,7 +1328,9 @@ export async function setAccountPaused(
|
|
|
1288
1328
|
if (paused && set.activeAccountId === accountId) {
|
|
1289
1329
|
const start = set.accounts.findIndex(candidate => candidate.id === accountId);
|
|
1290
1330
|
const ring = [...set.accounts.slice(start + 1), ...set.accounts.slice(0, start)];
|
|
1291
|
-
const fallback = ring.find(candidate => candidate.paused !== true && candidate.needsReauth !== true
|
|
1331
|
+
const fallback = ring.find(candidate => candidate.paused !== true && candidate.needsReauth !== true
|
|
1332
|
+
&& !(provider === "anthropic" && candidate.credential.source === "local-cli"
|
|
1333
|
+
&& candidate.credential.expires <= Date.now() + REFRESH_SKEW_MS));
|
|
1292
1334
|
if (fallback) {
|
|
1293
1335
|
set.activeAccountId = fallback.id;
|
|
1294
1336
|
set.selectionRevision = randomUUID();
|
|
@@ -1363,6 +1405,7 @@ export async function replaceProviderAccountSet(
|
|
|
1363
1405
|
...(account.needsReauth ? { needsReauth: true } : {}),
|
|
1364
1406
|
...(account.paused ? { paused: true } : {}),
|
|
1365
1407
|
...(account.autoSwitchThresholdOverride !== undefined ? { autoSwitchThresholdOverride: account.autoSwitchThresholdOverride } : {}),
|
|
1408
|
+
...(account.needsReauthReason === "verify_account" ? { needsReauthReason: account.needsReauthReason } : {}),
|
|
1366
1409
|
...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
|
|
1367
1410
|
...(account.loginId ? { loginId: account.loginId } : {}),
|
|
1368
1411
|
})),
|
|
@@ -1408,18 +1451,24 @@ export async function markAccountNeedsReauth(
|
|
|
1408
1451
|
provider: string,
|
|
1409
1452
|
accountId: string,
|
|
1410
1453
|
needsReauth: boolean,
|
|
1454
|
+
reason?: "verify_account",
|
|
1411
1455
|
writerGeneration = captureConfigGeneration(),
|
|
1412
1456
|
): Promise<void> {
|
|
1413
1457
|
if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(oauthAccountKey(provider, accountId))) return;
|
|
1414
1458
|
await mutateStore(store => {
|
|
1415
1459
|
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
1416
1460
|
if (!account) return;
|
|
1417
|
-
if (needsReauth)
|
|
1418
|
-
|
|
1461
|
+
if (needsReauth) {
|
|
1462
|
+
account.needsReauth = true;
|
|
1463
|
+
if (reason === "verify_account") account.needsReauthReason = reason;
|
|
1464
|
+
} else {
|
|
1465
|
+
delete account.needsReauth;
|
|
1466
|
+
delete account.needsReauthReason;
|
|
1467
|
+
}
|
|
1419
1468
|
}, [provider, accountId]);
|
|
1420
1469
|
}
|
|
1421
1470
|
|
|
1422
|
-
export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;delete account.needsReauth;return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
1471
|
+
export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
|
|
1423
1472
|
// A late refresh failure must not change an operator-paused account's health. Check under
|
|
1424
1473
|
// the mutation lock, not before awaiting it, so a concurrent pause cannot be overwritten.
|
|
1425
|
-
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration()):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;return true;},[provider,accountId,generation]);}
|
|
1474
|
+
export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account"):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
|
package/src/oauth/types.ts
CHANGED
|
@@ -62,6 +62,8 @@ export type OAuthCredentials = {
|
|
|
62
62
|
email?: string;
|
|
63
63
|
accountId?: string;
|
|
64
64
|
source?: OAuthCredentialSource;
|
|
65
|
+
/** Private authenticated account proof; never projected into account summaries. */
|
|
66
|
+
anthropicIdentity?: { v: 1; accountUuid: string; bearerSha256: string };
|
|
65
67
|
/** Google Antigravity (Cloud Code Assist) discovered project id; injected into the CCA envelope. */
|
|
66
68
|
projectId?: string;
|
|
67
69
|
/**
|
|
@@ -92,6 +94,13 @@ export interface ProviderAccount {
|
|
|
92
94
|
paused?: boolean;
|
|
93
95
|
/** Anthropic-only usage-switch override; absent inherits its pool default, zero disables it. */
|
|
94
96
|
autoSwitchThresholdOverride?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Why the account needs reauthentication. `verify_account` means the grant is
|
|
99
|
+
* alive but the provider blocks the account until the human verifies it
|
|
100
|
+
* (Antigravity 403 PERMISSION_DENIED) — a plain re-login without that
|
|
101
|
+
* verification will not help. Absent means an ordinary credential failure.
|
|
102
|
+
*/
|
|
103
|
+
needsReauthReason?: "verify_account";
|
|
95
104
|
addedAt?: number;
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -386,7 +386,8 @@ function createAnthropicMessageWriter(
|
|
|
386
386
|
return;
|
|
387
387
|
}
|
|
388
388
|
const message = typeof error.message === "string" ? error.message : "upstream request failed";
|
|
389
|
-
fail(anthropicFailedStatus(error, message), message, true
|
|
389
|
+
fail(anthropicFailedStatus(error, message), message, true,
|
|
390
|
+
error.code === "context_length_exceeded" ? error.code : undefined);
|
|
390
391
|
},
|
|
391
392
|
overflow() {
|
|
392
393
|
fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
|
|
@@ -413,7 +414,7 @@ export function encodeAnthropicMessageSse(
|
|
|
413
414
|
/**
|
|
414
415
|
* Fold an encoded Messages stream into the non-streaming client response, with the status
|
|
415
416
|
* mapping the Messages ingress applied to its collected message: a translator overflow is 413,
|
|
416
|
-
* a
|
|
417
|
+
* a classified context rejection is 400; other error frames/unexpected failures stay 502.
|
|
417
418
|
*/
|
|
418
419
|
export async function collectAnthropicMessageResponse(
|
|
419
420
|
stream: ReadableStream<Uint8Array>,
|
|
@@ -444,7 +445,7 @@ export async function collectAnthropicMessageResponse(
|
|
|
444
445
|
);
|
|
445
446
|
}
|
|
446
447
|
return new Response(JSON.stringify(message), {
|
|
447
|
-
status: isError ? 502 : 200,
|
|
448
|
+
status: isError ? (translatedError?.code === "context_length_exceeded" ? 400 : 502) : 200,
|
|
448
449
|
headers: { "Content-Type": "application/json" },
|
|
449
450
|
});
|
|
450
451
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { routedSlug, slugEquals, slugEquivalenceKey } from "./slug-codec";
|
|
3
|
+
|
|
4
|
+
/** Discovery evidence only; independent of the parser and the catalog cache. */
|
|
5
|
+
export interface AntigravityEffortFamilyRow {
|
|
6
|
+
id: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
custom?: boolean;
|
|
9
|
+
catalogKind?: string;
|
|
10
|
+
antigravityEffortWireModelIds?: Partial<Record<"low" | "medium" | "high", string>>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const FAMILY_EFFORTS = ["low", "medium", "high"] as const;
|
|
14
|
+
|
|
15
|
+
export function antigravityEffortFamilyIds(row: AntigravityEffortFamilyRow): string[] | undefined {
|
|
16
|
+
if (row.custom || row.catalogKind === "custom-model-v1" || !row.antigravityEffortWireModelIds) return undefined;
|
|
17
|
+
const ids = FAMILY_EFFORTS.map(effort => `${row.id}-${effort}`);
|
|
18
|
+
return FAMILY_EFFORTS.every((effort, index) => row.antigravityEffortWireModelIds![effort] === ids[index])
|
|
19
|
+
? ids : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Retained wire targets stay routable internally; only the public list collapses. */
|
|
23
|
+
export function collapseAntigravityPublicModels<T extends AntigravityEffortFamilyRow>(rows: T[]): T[] {
|
|
24
|
+
const hidden = new Map<string, Set<string>>();
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
const ids = antigravityEffortFamilyIds(row);
|
|
27
|
+
if (!ids) continue;
|
|
28
|
+
const providerIds = hidden.get(row.provider) ?? new Set<string>();
|
|
29
|
+
for (const id of ids) providerIds.add(id);
|
|
30
|
+
hidden.set(row.provider, providerIds);
|
|
31
|
+
}
|
|
32
|
+
return rows.filter(row => row.custom || row.catalogKind === "custom-model-v1"
|
|
33
|
+
|| antigravityEffortFamilyIds(row) !== undefined || !hidden.get(row.provider)?.has(row.id));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Project stored native/encoded selections without rewriting the user's allowlist. */
|
|
37
|
+
export function projectAntigravitySelectedModels(
|
|
38
|
+
provider: string,
|
|
39
|
+
selected: readonly string[],
|
|
40
|
+
rows: readonly AntigravityEffortFamilyRow[],
|
|
41
|
+
): string[] {
|
|
42
|
+
const projected = [...selected];
|
|
43
|
+
const keys = new Set(selected.map(id => slugEquivalenceKey(routedSlug(provider, id))));
|
|
44
|
+
const bases = new Set(rows.filter(row => row.provider === provider && antigravityEffortFamilyIds(row)).map(row => row.id));
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
if (row.provider !== provider) continue;
|
|
47
|
+
const ids = antigravityEffortFamilyIds(row);
|
|
48
|
+
const baseKey = slugEquivalenceKey(routedSlug(provider, row.id));
|
|
49
|
+
if (!ids || keys.has(baseKey) || projected.includes(row.id)
|
|
50
|
+
|| !ids.some(id => !bases.has(id) && keys.has(slugEquivalenceKey(routedSlug(provider, id))))) continue;
|
|
51
|
+
projected.push(row.id);
|
|
52
|
+
}
|
|
53
|
+
return projected;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Inherit once, before the baseline has ever recorded the public base identity. */
|
|
57
|
+
export function antigravityFamilyDisabled(
|
|
58
|
+
config: Pick<OcxConfig, "providers" | "disabledModels" | "modelDiscovery">,
|
|
59
|
+
row: AntigravityEffortFamilyRow,
|
|
60
|
+
rows: readonly AntigravityEffortFamilyRow[] = [],
|
|
61
|
+
): boolean {
|
|
62
|
+
const ids = antigravityEffortFamilyIds(row);
|
|
63
|
+
if (!ids) return false;
|
|
64
|
+
const disabled = config.disabledModels ?? [];
|
|
65
|
+
if (disabled.some(slug => slugEquals(slug, row.provider, row.id))) return true;
|
|
66
|
+
const baseline = config.modelDiscovery?.knownModels?.[row.provider];
|
|
67
|
+
if (!baseline) return false;
|
|
68
|
+
const known = new Set([...baseline.ids, ...baseline.removed]);
|
|
69
|
+
if (known.has(row.id)) return false;
|
|
70
|
+
const bases = new Set(rows.filter(candidate => candidate.provider === row.provider
|
|
71
|
+
&& antigravityEffortFamilyIds(candidate)).map(candidate => candidate.id));
|
|
72
|
+
const priorTiers = ids.filter(id => known.has(id) && !bases.has(id));
|
|
73
|
+
return priorTiers.length > 0
|
|
74
|
+
&& priorTiers.every(id => disabled.some(slug => slugEquals(slug, row.provider, id)));
|
|
75
|
+
}
|