@oh-my-pi/pi-ai 16.3.11 → 16.3.13
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/CHANGELOG.md +32 -0
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-retry.d.ts +3 -1
- package/dist/types/auth-storage.d.ts +13 -8
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/providers/cursor.d.ts +5 -0
- package/dist/types/providers/openai-shared.d.ts +6 -3
- package/dist/types/providers/register-builtins.d.ts +5 -0
- package/dist/types/registry/oauth/callback-server.d.ts +2 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +1 -7
- package/dist/types/registry/registry.d.ts +2 -1
- package/dist/types/registry/xai-oauth.d.ts +2 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/idle-iterator.d.ts +10 -0
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +20 -16
- package/src/auth-broker/remote-store.ts +55 -5
- package/src/auth-gateway/server.ts +1 -0
- package/src/auth-retry.ts +7 -2
- package/src/auth-storage.ts +210 -46
- package/src/providers/anthropic.ts +78 -8
- package/src/providers/cursor.ts +17 -10
- package/src/providers/openai-codex-responses.ts +37 -6
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/openai-shared.ts +30 -14
- package/src/providers/register-builtins.ts +15 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +55 -0
- package/src/registry/oauth/callback-server.ts +40 -12
- package/src/registry/oauth/xai-oauth.ts +6 -10
- package/src/registry/xai-oauth.ts +2 -1
- package/src/stream.ts +7 -1
- package/src/types.ts +2 -0
- package/src/usage/openai-codex.ts +3 -2
- package/src/utils/event-stream.ts +26 -0
- package/src/utils/idle-iterator.ts +65 -9
package/src/auth-storage.ts
CHANGED
|
@@ -66,6 +66,7 @@ const USAGE_RANKING_METRIC_EPSILON = 1e-9;
|
|
|
66
66
|
export type ApiKeyCredential = {
|
|
67
67
|
type: "api_key";
|
|
68
68
|
key: string;
|
|
69
|
+
source?: "login";
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
export type OAuthCredential = {
|
|
@@ -967,6 +968,7 @@ export class AuthStorage {
|
|
|
967
968
|
#usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
968
969
|
#rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
|
|
969
970
|
#usageCache: UsageCache;
|
|
971
|
+
#usageCacheEpoch = 0;
|
|
970
972
|
#usageRequestInFlight: Map<string, Promise<UsageReport | null>> = new Map();
|
|
971
973
|
#usageHeaderIngestAt: Map<string, number> = new Map();
|
|
972
974
|
#usageReportsInFlight: Map<string, Promise<UsageReport[] | null>> = new Map();
|
|
@@ -1390,12 +1392,14 @@ export class AuthStorage {
|
|
|
1390
1392
|
|
|
1391
1393
|
const credentialId = this.#getStoredCredentials(provider)[credentialIndex]?.id;
|
|
1392
1394
|
if (credentialId === undefined) return blockedUntil;
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1395
|
+
if (!blockScope || provider !== "openai-codex") {
|
|
1396
|
+
const persistedGlobalBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, "");
|
|
1397
|
+
if (
|
|
1398
|
+
persistedGlobalBlockedUntil !== undefined &&
|
|
1399
|
+
(blockedUntil === undefined || persistedGlobalBlockedUntil > blockedUntil)
|
|
1400
|
+
) {
|
|
1401
|
+
blockedUntil = persistedGlobalBlockedUntil;
|
|
1402
|
+
}
|
|
1399
1403
|
}
|
|
1400
1404
|
if (blockScope) {
|
|
1401
1405
|
const persistedScopedBlockedUntil = this.#readPersistedCredentialBlock(credentialId, providerKey, blockScope);
|
|
@@ -1433,6 +1437,7 @@ export class AuthStorage {
|
|
|
1433
1437
|
const nextBlockedUntil = Math.max(existing, blockedUntilMs);
|
|
1434
1438
|
backoffMap.set(credentialIndex, nextBlockedUntil);
|
|
1435
1439
|
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
1440
|
+
this.#invalidateUsageReportCache(provider);
|
|
1436
1441
|
|
|
1437
1442
|
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
1438
1443
|
if (!upsertCredentialBlock) return;
|
|
@@ -1554,13 +1559,14 @@ export class AuthStorage {
|
|
|
1554
1559
|
provider: string,
|
|
1555
1560
|
type: T,
|
|
1556
1561
|
sessionId?: string,
|
|
1562
|
+
filter?: (credential: AuthCredential) => boolean,
|
|
1557
1563
|
): { credential: Extract<AuthCredential, { type: T }>; index: number } | undefined {
|
|
1558
1564
|
const credentials = this.#getCredentialsForProvider(provider)
|
|
1559
1565
|
.map((credential, index) => ({ credential, index }))
|
|
1560
|
-
.filter(
|
|
1561
|
-
(entry
|
|
1562
|
-
|
|
1563
|
-
);
|
|
1566
|
+
.filter((entry): entry is { credential: Extract<AuthCredential, { type: T }>; index: number } => {
|
|
1567
|
+
if (entry.credential.type !== type) return false;
|
|
1568
|
+
return filter?.(entry.credential) ?? true;
|
|
1569
|
+
});
|
|
1564
1570
|
|
|
1565
1571
|
if (credentials.length === 0) return undefined;
|
|
1566
1572
|
if (credentials.length === 1) return credentials[0];
|
|
@@ -1851,8 +1857,8 @@ export class AuthStorage {
|
|
|
1851
1857
|
/**
|
|
1852
1858
|
* Classify where a provider's auth comes from, following the same precedence
|
|
1853
1859
|
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
|
1854
|
-
* stored OAuth → env var → stored api_key →
|
|
1855
|
-
* undefined when no auth is configured.
|
|
1860
|
+
* stored OAuth → login-stored api_key → env var → stored api_key →
|
|
1861
|
+
* fallback resolver. Returns undefined when no auth is configured.
|
|
1856
1862
|
*
|
|
1857
1863
|
* Compact, structured counterpart to {@link describeCredentialSource}.
|
|
1858
1864
|
*/
|
|
@@ -1861,6 +1867,9 @@ export class AuthStorage {
|
|
|
1861
1867
|
if (this.#configOverrides.has(provider)) return { kind: "config" };
|
|
1862
1868
|
const stored = this.#getCredentialsForProvider(provider);
|
|
1863
1869
|
if (stored.some(credential => credential.type === "oauth")) return { kind: "oauth" };
|
|
1870
|
+
if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) {
|
|
1871
|
+
return { kind: "api_key" };
|
|
1872
|
+
}
|
|
1864
1873
|
if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
|
1865
1874
|
if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" };
|
|
1866
1875
|
if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
|
|
@@ -2005,7 +2014,7 @@ export class AuthStorage {
|
|
|
2005
2014
|
if (!result) {
|
|
2006
2015
|
return;
|
|
2007
2016
|
}
|
|
2008
|
-
const newCredential: ApiKeyCredential = { type: "api_key", key: result };
|
|
2017
|
+
const newCredential: ApiKeyCredential = { type: "api_key", key: result, source: "login" };
|
|
2009
2018
|
const stored = this.#store.upsertAuthCredentialRemote
|
|
2010
2019
|
? await this.#store.upsertAuthCredentialRemote(provider, newCredential)
|
|
2011
2020
|
: this.#store.upsertAuthCredentialForProvider(provider, newCredential);
|
|
@@ -2336,8 +2345,10 @@ export class AuthStorage {
|
|
|
2336
2345
|
const inFlight = this.#usageRequestInFlight.get(cacheKey);
|
|
2337
2346
|
if (inFlight) return inFlight;
|
|
2338
2347
|
|
|
2348
|
+
const usageCacheEpoch = this.#usageCacheEpoch;
|
|
2339
2349
|
const promise = (async () => {
|
|
2340
2350
|
const report = await this.#fetchUsageUncached(request, timeoutMs);
|
|
2351
|
+
if (usageCacheEpoch !== this.#usageCacheEpoch) return report;
|
|
2341
2352
|
const ttlJitter = USAGE_REPORT_TTL_MS * (Math.random() * 0.5 - 0.25);
|
|
2342
2353
|
if (report !== null) {
|
|
2343
2354
|
// Success: stagger per-credential cache expiry so all accounts don't
|
|
@@ -2347,6 +2358,7 @@ export class AuthStorage {
|
|
|
2347
2358
|
// times decorrelate within a few cycles.
|
|
2348
2359
|
this.#usageCache.set(cacheKey, { value: report, expiresAt: Date.now() + USAGE_REPORT_TTL_MS + ttlJitter });
|
|
2349
2360
|
this.#recordUsageHistory(request, report);
|
|
2361
|
+
this.#reconcileCodexUsageBlock(request, report);
|
|
2350
2362
|
return report;
|
|
2351
2363
|
}
|
|
2352
2364
|
// Failure: apply a short jittered cool-down so the credential doesn't
|
|
@@ -2761,7 +2773,14 @@ export class AuthStorage {
|
|
|
2761
2773
|
// whole point of routing through it.
|
|
2762
2774
|
const storeHook = this.#store.getUsageReport?.bind(this.#store);
|
|
2763
2775
|
if (storeHook) {
|
|
2764
|
-
|
|
2776
|
+
const report = await storeHook(provider, credential, options?.signal);
|
|
2777
|
+
if (report) {
|
|
2778
|
+
this.#reconcileCodexUsageBlock(
|
|
2779
|
+
this.#buildUsageRequestForOauth(provider, credential, options?.baseUrl),
|
|
2780
|
+
report,
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
return report;
|
|
2765
2784
|
}
|
|
2766
2785
|
return this.#fetchUsageCached(
|
|
2767
2786
|
this.#buildUsageRequestForOauth(provider, credential, options?.baseUrl),
|
|
@@ -2789,7 +2808,10 @@ export class AuthStorage {
|
|
|
2789
2808
|
// `RemoteAuthCredentialStore` implements the store hook so a gateway
|
|
2790
2809
|
// backed by a broker automatically routes usage to the broker without
|
|
2791
2810
|
// needing the caller to wire it explicitly.
|
|
2792
|
-
const
|
|
2811
|
+
const storeOverride = this.#store.fetchUsageReports?.bind(this.#store);
|
|
2812
|
+
const override = this.#fetchUsageReportsOverride ?? storeOverride;
|
|
2813
|
+
const shouldReconcileStoreHookReports =
|
|
2814
|
+
this.#fetchUsageReportsOverride === undefined && storeOverride !== undefined;
|
|
2793
2815
|
if (override) {
|
|
2794
2816
|
// Reuse the in-flight map so concurrent callers (widget poll + format
|
|
2795
2817
|
// dispatch + credential selection) coalesce into one upstream call.
|
|
@@ -2805,7 +2827,9 @@ export class AuthStorage {
|
|
|
2805
2827
|
});
|
|
2806
2828
|
this.#usageReportsInFlight.set(OVERRIDE_KEY, shared);
|
|
2807
2829
|
}
|
|
2808
|
-
|
|
2830
|
+
const reports = await raceUsageWithSignal(shared, options?.signal);
|
|
2831
|
+
if (shouldReconcileStoreHookReports && reports) this.#reconcileCodexUsageBlocksFromReports(reports);
|
|
2832
|
+
return reports;
|
|
2809
2833
|
}
|
|
2810
2834
|
if (!this.#usageProviderResolver) return null;
|
|
2811
2835
|
|
|
@@ -3057,9 +3081,20 @@ export class AuthStorage {
|
|
|
3057
3081
|
async markUsageLimitReached(
|
|
3058
3082
|
provider: string,
|
|
3059
3083
|
sessionId: string | undefined,
|
|
3060
|
-
options?: { retryAfterMs?: number; baseUrl?: string; modelId?: string; signal?: AbortSignal },
|
|
3084
|
+
options?: { retryAfterMs?: number; baseUrl?: string; modelId?: string; apiKey?: string; signal?: AbortSignal },
|
|
3061
3085
|
): Promise<UsageLimitMarkResult> {
|
|
3062
|
-
|
|
3086
|
+
let sessionCredential: { type: AuthCredential["type"]; index: number } | undefined;
|
|
3087
|
+
if (options?.apiKey) {
|
|
3088
|
+
const stored = this.#getStoredCredentials(provider);
|
|
3089
|
+
for (let index = 0; index < stored.length; index++) {
|
|
3090
|
+
const entry = stored[index];
|
|
3091
|
+
if (entry && (await this.#credentialMatchesApiKey(entry.credential, options.apiKey))) {
|
|
3092
|
+
sessionCredential = { type: entry.credential.type, index };
|
|
3093
|
+
break;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
sessionCredential ??= this.#getSessionCredential(provider, sessionId);
|
|
3063
3098
|
if (!sessionCredential) return { switched: false };
|
|
3064
3099
|
|
|
3065
3100
|
const providerKey = this.#getProviderTypeKey(provider, sessionCredential.type);
|
|
@@ -3387,12 +3422,21 @@ export class AuthStorage {
|
|
|
3387
3422
|
const checkUsage = strategy !== undefined && (credentials.length > 1 || requiresProModel);
|
|
3388
3423
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
3389
3424
|
const sessionPreferredIndex = sessionCredential?.type === "oauth" ? sessionCredential.index : undefined;
|
|
3425
|
+
const sessionPreferredCredential =
|
|
3426
|
+
sessionPreferredIndex !== undefined
|
|
3427
|
+
? credentials.find(entry => entry.index === sessionPreferredIndex)?.credential
|
|
3428
|
+
: undefined;
|
|
3429
|
+
const sessionPreferredCanRefreshOrUse =
|
|
3430
|
+
sessionPreferredCredential !== undefined &&
|
|
3431
|
+
(sessionPreferredCredential.refresh.trim().length > 0 ||
|
|
3432
|
+
Date.now() + OAUTH_REFRESH_SKEW_MS < sessionPreferredCredential.expires);
|
|
3390
3433
|
// Skip ranking only when the session already has a working preferred credential — re-ranking
|
|
3391
3434
|
// mid-session causes account switches that cold-start the server-side prompt cache. New sessions
|
|
3392
3435
|
// (no preference) and sessions whose preferred is blocked still rank, so we pick the account
|
|
3393
3436
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
3394
3437
|
const sessionPreferredIsAvailable =
|
|
3395
3438
|
sessionPreferredIndex !== undefined &&
|
|
3439
|
+
sessionPreferredCanRefreshOrUse &&
|
|
3396
3440
|
!this.#isCredentialBlocked(provider, providerKey, sessionPreferredIndex, blockScope);
|
|
3397
3441
|
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
3398
3442
|
const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
|
|
@@ -3899,8 +3943,8 @@ export class AuthStorage {
|
|
|
3899
3943
|
return configKey;
|
|
3900
3944
|
}
|
|
3901
3945
|
|
|
3902
|
-
// Precedence: a deliberate OAuth
|
|
3903
|
-
// static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
3946
|
+
// Precedence: a deliberate OAuth/login credential wins, then an explicit env var,
|
|
3947
|
+
// then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
3904
3948
|
const oauthSelection = this.#selectCredentialByType(provider, "oauth");
|
|
3905
3949
|
if (oauthSelection) {
|
|
3906
3950
|
const expiresAt = oauthSelection.credential.expires;
|
|
@@ -3916,6 +3960,16 @@ export class AuthStorage {
|
|
|
3916
3960
|
}
|
|
3917
3961
|
}
|
|
3918
3962
|
|
|
3963
|
+
const loginApiKeySelection = this.#selectCredentialByType(
|
|
3964
|
+
provider,
|
|
3965
|
+
"api_key",
|
|
3966
|
+
undefined,
|
|
3967
|
+
credential => credential.type === "api_key" && credential.source === "login",
|
|
3968
|
+
);
|
|
3969
|
+
if (loginApiKeySelection) {
|
|
3970
|
+
return this.#configValueResolver(loginApiKeySelection.credential.key);
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3919
3973
|
const envKey = getEnvApiKey(provider);
|
|
3920
3974
|
if (envKey) return envKey;
|
|
3921
3975
|
|
|
@@ -3933,9 +3987,10 @@ export class AuthStorage {
|
|
|
3933
3987
|
* 1. Runtime override (CLI --api-key)
|
|
3934
3988
|
* 2. Config override (models.yml `providers.<name>.apiKey`)
|
|
3935
3989
|
* 3. OAuth token from storage (auto-refreshed)
|
|
3936
|
-
* 4.
|
|
3937
|
-
* 5.
|
|
3938
|
-
* 6.
|
|
3990
|
+
* 4. API key persisted by a successful `/login`
|
|
3991
|
+
* 5. Environment variable
|
|
3992
|
+
* 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
3993
|
+
* 7. Fallback resolver (models.yml custom providers, last-resort)
|
|
3939
3994
|
*/
|
|
3940
3995
|
async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
3941
3996
|
// Runtime override takes highest priority
|
|
@@ -3954,13 +4009,24 @@ export class AuthStorage {
|
|
|
3954
4009
|
return configKey;
|
|
3955
4010
|
}
|
|
3956
4011
|
|
|
3957
|
-
// Precedence: a deliberate OAuth
|
|
3958
|
-
// static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
4012
|
+
// Precedence: a deliberate OAuth/login credential wins, then an explicit env var,
|
|
4013
|
+
// then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
3959
4014
|
const oauthResolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
3960
4015
|
if (oauthResolved) {
|
|
3961
4016
|
return oauthResolved.apiKey;
|
|
3962
4017
|
}
|
|
3963
4018
|
|
|
4019
|
+
const loginApiKeySelection = this.#selectCredentialByType(
|
|
4020
|
+
provider,
|
|
4021
|
+
"api_key",
|
|
4022
|
+
sessionId,
|
|
4023
|
+
credential => credential.type === "api_key" && credential.source === "login",
|
|
4024
|
+
);
|
|
4025
|
+
if (loginApiKeySelection) {
|
|
4026
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", loginApiKeySelection.index);
|
|
4027
|
+
return this.#configValueResolver(loginApiKeySelection.credential.key);
|
|
4028
|
+
}
|
|
4029
|
+
|
|
3964
4030
|
// Past OAuth: the session sticky (if any) is stale — the request authenticates via
|
|
3965
4031
|
// env/api_key/fallback, not OAuth, so clear it now so getOAuthAccountId() correctly
|
|
3966
4032
|
// suppresses account_uuid for this session.
|
|
@@ -3969,7 +4035,12 @@ export class AuthStorage {
|
|
|
3969
4035
|
const envKey = getEnvApiKey(provider);
|
|
3970
4036
|
if (envKey) return envKey;
|
|
3971
4037
|
|
|
3972
|
-
const apiKeySelection = this.#selectCredentialByType(
|
|
4038
|
+
const apiKeySelection = this.#selectCredentialByType(
|
|
4039
|
+
provider,
|
|
4040
|
+
"api_key",
|
|
4041
|
+
sessionId,
|
|
4042
|
+
credential => credential.type !== "api_key" || credential.source !== "login",
|
|
4043
|
+
);
|
|
3973
4044
|
if (apiKeySelection) {
|
|
3974
4045
|
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
3975
4046
|
return this.#configValueResolver(apiKeySelection.credential.key);
|
|
@@ -4255,6 +4326,7 @@ export class AuthStorage {
|
|
|
4255
4326
|
* `/usage` reflects a freshly-redeemed reset instead of stale numbers.
|
|
4256
4327
|
*/
|
|
4257
4328
|
#invalidateUsageReportCache(provider: string, baseUrl?: string): void {
|
|
4329
|
+
this.#usageCacheEpoch += 1;
|
|
4258
4330
|
const expired = Date.now() - 1;
|
|
4259
4331
|
for (const entry of this.#getStoredCredentials(provider)) {
|
|
4260
4332
|
if (entry.credential.type !== "oauth") continue;
|
|
@@ -4266,6 +4338,12 @@ export class AuthStorage {
|
|
|
4266
4338
|
}
|
|
4267
4339
|
}
|
|
4268
4340
|
|
|
4341
|
+
#invalidateUsageReportCacheForProviderKey(providerKey: string): void {
|
|
4342
|
+
const oauthSuffix = ":oauth";
|
|
4343
|
+
if (!providerKey.endsWith(oauthSuffix)) return;
|
|
4344
|
+
this.#invalidateUsageReportCache(providerKey.slice(0, -oauthSuffix.length));
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4269
4347
|
/**
|
|
4270
4348
|
* Lift any temporary backoff blocks on one credential (across the bare
|
|
4271
4349
|
* `provider:oauth` key and its scoped `\0`-suffixed derivatives). Called
|
|
@@ -4274,13 +4352,10 @@ export class AuthStorage {
|
|
|
4274
4352
|
* that `markUsageLimitReached` set for the now-obsolete reset time.
|
|
4275
4353
|
*/
|
|
4276
4354
|
#clearCredentialBlocks(provider: string, credentialId: number): void {
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
} catch (err) {
|
|
4282
|
-
logger.debug("Failed to clear persisted credential blocks", { err, provider, credentialId });
|
|
4283
|
-
}
|
|
4355
|
+
try {
|
|
4356
|
+
this.deleteCredentialBlocks(credentialId);
|
|
4357
|
+
} catch (err) {
|
|
4358
|
+
logger.debug("Failed to clear persisted credential blocks", { err, provider, credentialId });
|
|
4284
4359
|
}
|
|
4285
4360
|
|
|
4286
4361
|
const index = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
@@ -4294,6 +4369,80 @@ export class AuthStorage {
|
|
|
4294
4369
|
}
|
|
4295
4370
|
}
|
|
4296
4371
|
|
|
4372
|
+
/**
|
|
4373
|
+
* Self-heal a stale Codex usage-limit block: when a fresh live usage report
|
|
4374
|
+
* shows the account is allowed and below every limit, drop the persisted and
|
|
4375
|
+
* in-memory `openai-codex:oauth` blocks so the balancer re-includes it. Only
|
|
4376
|
+
* Codex — its ranking strategy uses the single model-independent `"shared"`
|
|
4377
|
+
* scope, so clearing every block for the credential id is exact.
|
|
4378
|
+
*/
|
|
4379
|
+
#isHealthyCodexUsageReport(report: UsageReport): boolean {
|
|
4380
|
+
const metadata = report.metadata;
|
|
4381
|
+
return (
|
|
4382
|
+
report.provider === "openai-codex" &&
|
|
4383
|
+
metadata?.allowed === true &&
|
|
4384
|
+
metadata.limitReached === false &&
|
|
4385
|
+
!this.#isUsageLimitReached(report.limits)
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
#reconcileCodexUsageBlockForCredential(provider: Provider, credentialId: number, report: UsageReport): void {
|
|
4390
|
+
if (!this.#isHealthyCodexUsageReport(report)) return;
|
|
4391
|
+
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
4392
|
+
const credentialIndex = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
4393
|
+
if (credentialIndex < 0) return;
|
|
4394
|
+
// Mirror selection: consult the same strategy scope `markUsageLimitReached`
|
|
4395
|
+
// persists under, else a scoped block is invisible here and never healed.
|
|
4396
|
+
const blockScope = this.#rankingStrategyResolver?.(provider)?.blockScope?.({});
|
|
4397
|
+
const blockedUntilMs = this.#getCredentialBlockedUntil(provider, providerKey, credentialIndex, blockScope);
|
|
4398
|
+
if (blockedUntilMs === undefined) return;
|
|
4399
|
+
this.#clearCredentialBlocks(provider, credentialId);
|
|
4400
|
+
logger.info("Cleared stale Codex usage-limit block after healthy live usage report", {
|
|
4401
|
+
credentialId,
|
|
4402
|
+
provider,
|
|
4403
|
+
clearedBlockedUntilMs: blockedUntilMs,
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
#reconcileCodexUsageBlock(request: UsageRequestDescriptor, report: UsageReport): void {
|
|
4408
|
+
if (request.provider !== "openai-codex") return;
|
|
4409
|
+
const credentialId = this.#findStoredCredentialIdForUsageCredential(request.provider, request.credential);
|
|
4410
|
+
if (credentialId === undefined) return;
|
|
4411
|
+
this.#reconcileCodexUsageBlockForCredential(request.provider, credentialId, report);
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
#findStoredCredentialIdsForUsageReport(report: UsageReport): number[] {
|
|
4415
|
+
if (report.provider !== "openai-codex") return [];
|
|
4416
|
+
const email = this.#getUsageReportMetadataValue(report, "email")?.toLowerCase();
|
|
4417
|
+
const accountId = (
|
|
4418
|
+
this.#getUsageReportMetadataValue(report, "accountId") ?? this.#getUsageReportScopeAccountId(report)
|
|
4419
|
+
)?.toLowerCase();
|
|
4420
|
+
if (!email && !accountId) return [];
|
|
4421
|
+
const matches: number[] = [];
|
|
4422
|
+
for (const entry of this.#getStoredCredentials(report.provider)) {
|
|
4423
|
+
const credential = entry.credential;
|
|
4424
|
+
if (credential.type !== "oauth") continue;
|
|
4425
|
+
const credentialEmail = credential.email?.trim().toLowerCase();
|
|
4426
|
+
const credentialAccountId = credential.accountId?.trim().toLowerCase();
|
|
4427
|
+
if ((email && credentialEmail === email) || (accountId && credentialAccountId === accountId)) {
|
|
4428
|
+
matches.push(entry.id);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return matches;
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
#reconcileCodexUsageBlocksFromReports(reports: UsageReport[]): void {
|
|
4435
|
+
const reconciled = new Set<number>();
|
|
4436
|
+
for (const report of reports) {
|
|
4437
|
+
if (!this.#isHealthyCodexUsageReport(report)) continue;
|
|
4438
|
+
for (const credentialId of this.#findStoredCredentialIdsForUsageReport(report)) {
|
|
4439
|
+
if (reconciled.has(credentialId)) continue;
|
|
4440
|
+
reconciled.add(credentialId);
|
|
4441
|
+
this.#reconcileCodexUsageBlockForCredential(report.provider, credentialId, report);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4297
4446
|
#extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
4298
4447
|
if (!apiKey.startsWith("{")) return undefined;
|
|
4299
4448
|
try {
|
|
@@ -4381,7 +4530,7 @@ export class AuthStorage {
|
|
|
4381
4530
|
async rotateSessionCredential(
|
|
4382
4531
|
provider: string,
|
|
4383
4532
|
sessionId: string | undefined,
|
|
4384
|
-
options?: { error?: unknown; modelId?: string; signal?: AbortSignal },
|
|
4533
|
+
options?: { error?: unknown; modelId?: string; apiKey?: string; signal?: AbortSignal },
|
|
4385
4534
|
): Promise<boolean> {
|
|
4386
4535
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
4387
4536
|
if (!sessionCredential) return false;
|
|
@@ -4393,6 +4542,7 @@ export class AuthStorage {
|
|
|
4393
4542
|
return (
|
|
4394
4543
|
await this.markUsageLimitReached(provider, sessionId, {
|
|
4395
4544
|
modelId: options?.modelId,
|
|
4545
|
+
apiKey: options?.apiKey,
|
|
4396
4546
|
signal: options?.signal,
|
|
4397
4547
|
})
|
|
4398
4548
|
).switched;
|
|
@@ -4654,6 +4804,7 @@ export class AuthStorage {
|
|
|
4654
4804
|
const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
|
|
4655
4805
|
if (!upsertCredentialBlock) return;
|
|
4656
4806
|
upsertCredentialBlock(block);
|
|
4807
|
+
this.#invalidateUsageReportCacheForProviderKey(block.providerKey);
|
|
4657
4808
|
this.#bumpGeneration("credential-block");
|
|
4658
4809
|
}
|
|
4659
4810
|
|
|
@@ -4674,9 +4825,10 @@ export class AuthStorage {
|
|
|
4674
4825
|
* 1. Runtime override (`--api-key`).
|
|
4675
4826
|
* 2. Config override (`models.yml` `providers.<name>.apiKey`).
|
|
4676
4827
|
* 3. Stored OAuth credential.
|
|
4677
|
-
* 4.
|
|
4678
|
-
* 5.
|
|
4679
|
-
* 6.
|
|
4828
|
+
* 4. API key persisted by a successful `/login`.
|
|
4829
|
+
* 5. Env var — overrides a stored static api_key (e.g. a stale broker copy).
|
|
4830
|
+
* 6. Stored api_key credential.
|
|
4831
|
+
* 7. Fallback resolver.
|
|
4680
4832
|
*
|
|
4681
4833
|
* The string is purely informational; consumers must not parse it.
|
|
4682
4834
|
*/
|
|
@@ -4691,14 +4843,16 @@ export class AuthStorage {
|
|
|
4691
4843
|
const baseLabel = this.#sourceLabel ?? "local store";
|
|
4692
4844
|
const stored = this.#getStoredCredentials(provider);
|
|
4693
4845
|
const session = sessionId ? this.#sessionLastCredential.get(provider)?.get(sessionId) : undefined;
|
|
4694
|
-
|
|
4695
|
-
|
|
4846
|
+
const describeStored = (
|
|
4847
|
+
type: AuthCredential["type"],
|
|
4848
|
+
filter?: (credential: AuthCredential) => boolean,
|
|
4849
|
+
): string | undefined => {
|
|
4696
4850
|
const typed = stored
|
|
4697
4851
|
.map((entry, index) => ({ entry, index }))
|
|
4698
|
-
.filter(({ entry }) => entry.credential.type === type);
|
|
4852
|
+
.filter(({ entry }) => entry.credential.type === type && (filter?.(entry.credential) ?? true));
|
|
4699
4853
|
if (typed.length === 0) return undefined;
|
|
4700
|
-
const
|
|
4701
|
-
const chosen =
|
|
4854
|
+
const sticky = session?.type === type ? typed.find(entry => entry.index === session.index) : undefined;
|
|
4855
|
+
const chosen = sticky?.entry ?? typed[0].entry;
|
|
4702
4856
|
const credential = chosen.credential;
|
|
4703
4857
|
const identity =
|
|
4704
4858
|
credential.type === "oauth"
|
|
@@ -4707,11 +4861,19 @@ export class AuthStorage {
|
|
|
4707
4861
|
return `${baseLabel} · ${type} #${chosen.id} (${identity})`;
|
|
4708
4862
|
};
|
|
4709
4863
|
|
|
4710
|
-
//
|
|
4864
|
+
// Deliberate login credentials win; then an explicit env var; then a stored static api_key.
|
|
4711
4865
|
const oauthSource = describeStored("oauth");
|
|
4712
4866
|
if (oauthSource) return oauthSource;
|
|
4867
|
+
const loginApiKeySource = describeStored(
|
|
4868
|
+
"api_key",
|
|
4869
|
+
credential => credential.type === "api_key" && credential.source === "login",
|
|
4870
|
+
);
|
|
4871
|
+
if (loginApiKeySource) return loginApiKeySource;
|
|
4713
4872
|
if (getEnvApiKey(provider)) return `env (over ${baseLabel})`;
|
|
4714
|
-
const apiKeySource = describeStored(
|
|
4873
|
+
const apiKeySource = describeStored(
|
|
4874
|
+
"api_key",
|
|
4875
|
+
credential => credential.type !== "api_key" || credential.source !== "login",
|
|
4876
|
+
);
|
|
4715
4877
|
if (apiKeySource) return apiKeySource;
|
|
4716
4878
|
if (this.#fallbackResolver?.(provider) !== undefined) return "fallback resolver";
|
|
4717
4879
|
return undefined;
|
|
@@ -4776,9 +4938,10 @@ function normalizeStoredIdentityKey(identityKey: string | null | undefined): str
|
|
|
4776
4938
|
|
|
4777
4939
|
function serializeCredential(provider: string, credential: AuthCredential): SerializedCredentialRecord | null {
|
|
4778
4940
|
if (credential.type === "api_key") {
|
|
4941
|
+
const data = credential.source === "login" ? { key: credential.key, source: "login" } : { key: credential.key };
|
|
4779
4942
|
return {
|
|
4780
4943
|
credentialType: "api_key",
|
|
4781
|
-
data: JSON.stringify(
|
|
4944
|
+
data: JSON.stringify(data),
|
|
4782
4945
|
identityKey: null,
|
|
4783
4946
|
};
|
|
4784
4947
|
}
|
|
@@ -4806,7 +4969,8 @@ function deserializeCredential(row: AuthRow): AuthCredential | null {
|
|
|
4806
4969
|
if (row.credential_type === "api_key") {
|
|
4807
4970
|
const data = parsed as Record<string, unknown>;
|
|
4808
4971
|
if (typeof data.key === "string") {
|
|
4809
|
-
|
|
4972
|
+
const source = data.source === "login" ? "login" : undefined;
|
|
4973
|
+
return source ? { type: "api_key", key: data.key, source } : { type: "api_key", key: data.key };
|
|
4810
4974
|
}
|
|
4811
4975
|
}
|
|
4812
4976
|
if (row.credential_type === "oauth") {
|
|
@@ -127,12 +127,13 @@ export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readon
|
|
|
127
127
|
|
|
128
128
|
const midConversationSystemBeta = "mid-conversation-system-2026-04-07";
|
|
129
129
|
const contextManagementBeta = "context-management-2025-06-27";
|
|
130
|
+
const structuredOutputsBeta = "structured-outputs-2025-12-15";
|
|
130
131
|
const claudeCodeUtilityBetaDefaults = [
|
|
131
132
|
"oauth-2025-04-20",
|
|
132
133
|
"interleaved-thinking-2025-05-14",
|
|
133
134
|
contextManagementBeta,
|
|
134
135
|
"prompt-caching-scope-2026-01-05",
|
|
135
|
-
|
|
136
|
+
structuredOutputsBeta,
|
|
136
137
|
] as const;
|
|
137
138
|
const claudeCodeAgentBetaDefaults = [
|
|
138
139
|
"claude-code-20250219",
|
|
@@ -159,10 +160,12 @@ function buildClaudeCodeBetas(
|
|
|
159
160
|
agentRequest: boolean,
|
|
160
161
|
thinkingRequest: boolean,
|
|
161
162
|
redactThinking: boolean,
|
|
163
|
+
disableStrictTools = false,
|
|
162
164
|
): readonly string[] {
|
|
163
|
-
if (!agentRequest && !redactThinking) return claudeCodeUtilityBetaDefaults;
|
|
165
|
+
if (!agentRequest && !redactThinking && !disableStrictTools) return claudeCodeUtilityBetaDefaults;
|
|
164
166
|
const betas: string[] = [];
|
|
165
167
|
for (const beta of agentRequest ? claudeCodeAgentBetaDefaults : claudeCodeUtilityBetaDefaults) {
|
|
168
|
+
if (disableStrictTools && beta === structuredOutputsBeta) continue;
|
|
166
169
|
betas.push(beta);
|
|
167
170
|
// Match CC's header order: redact-thinking immediately follows interleaved-thinking.
|
|
168
171
|
if (redactThinking && beta === interleavedThinkingBeta) betas.push(redactThinkingBeta);
|
|
@@ -1098,6 +1101,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
1098
1101
|
hasTools?: boolean;
|
|
1099
1102
|
thinkingEnabled?: boolean;
|
|
1100
1103
|
thinkingDisplay?: AnthropicThinkingDisplay;
|
|
1104
|
+
disableStrictTools?: boolean;
|
|
1101
1105
|
fetch?: FetchImpl;
|
|
1102
1106
|
claudeCodeSessionId?: string;
|
|
1103
1107
|
};
|
|
@@ -1458,6 +1462,16 @@ async function* observeDecodedAnthropicSdkEvents(
|
|
|
1458
1462
|
|
|
1459
1463
|
const PROVIDER_MAX_RETRIES = 10;
|
|
1460
1464
|
|
|
1465
|
+
/**
|
|
1466
|
+
* How long `ping` keepalives may keep extending the idle deadline without any
|
|
1467
|
+
* semantic stream progress, as a multiple of the idle timeout. Anthropic pings
|
|
1468
|
+
* across legitimate generation gaps, so pings count as liveness — but a wedged
|
|
1469
|
+
* upstream that pings forever while producing no events must eventually trip
|
|
1470
|
+
* the idle watchdog instead of hanging an active tool-call stream without a
|
|
1471
|
+
* recovery path (#4900).
|
|
1472
|
+
*/
|
|
1473
|
+
const PING_PROGRESS_MAX_IDLE_MULTIPLIER = 3;
|
|
1474
|
+
|
|
1461
1475
|
/**
|
|
1462
1476
|
* Log a malformed-stream-envelope anomaly without aborting the turn. The strict
|
|
1463
1477
|
* parser would `throw new AnthropicStreamEnvelopeError(...)` here; we instead
|
|
@@ -1833,6 +1847,7 @@ const streamAnthropicOnce = (
|
|
|
1833
1847
|
thinkingDisplay: options?.thinkingDisplay,
|
|
1834
1848
|
fetch: options?.fetch,
|
|
1835
1849
|
claudeCodeSessionId: options?.sessionId ?? extractClaudeMetadataSessionId(options?.metadata?.user_id),
|
|
1850
|
+
disableStrictTools,
|
|
1836
1851
|
});
|
|
1837
1852
|
client = created.client;
|
|
1838
1853
|
isOAuthToken = created.isOAuthToken;
|
|
@@ -2002,11 +2017,20 @@ const streamAnthropicOnce = (
|
|
|
2002
2017
|
}
|
|
2003
2018
|
>();
|
|
2004
2019
|
|
|
2005
|
-
// Pings keep the idle deadline alive once content is flowing
|
|
2006
|
-
//
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2020
|
+
// Pings keep the idle deadline alive once content is flowing (Anthropic
|
|
2021
|
+
// bridges legitimate generation gaps with keepalives), but only within a
|
|
2022
|
+
// bounded window: a wedged upstream that pings forever while the model
|
|
2023
|
+
// produces nothing must still trip the idle watchdog, otherwise an
|
|
2024
|
+
// active tool-call stream hangs unrecoverably with no retry (#4900).
|
|
2025
|
+
// A ping before message_start must not consume the first-event watchdog
|
|
2026
|
+
// either: it would flip the (retryable) pre-content stall classification
|
|
2027
|
+
// into a terminal mid-stream idle timeout.
|
|
2009
2028
|
let sawNonPingEvent = false;
|
|
2029
|
+
let lastNonPingProgressAtMs = 0;
|
|
2030
|
+
const pingProgressCapMs =
|
|
2031
|
+
idleTimeoutMs !== undefined && idleTimeoutMs > 0
|
|
2032
|
+
? idleTimeoutMs * PING_PROGRESS_MAX_IDLE_MULTIPLIER
|
|
2033
|
+
: undefined;
|
|
2010
2034
|
const timedAnthropicStream = iterateWithIdleTimeout(anthropicStream, {
|
|
2011
2035
|
idleTimeoutMs,
|
|
2012
2036
|
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
@@ -2016,8 +2040,13 @@ const streamAnthropicOnce = (
|
|
|
2016
2040
|
onFirstItemTimeout: () => activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
|
|
2017
2041
|
abortSignal: options?.signal,
|
|
2018
2042
|
isProgressItem: item => {
|
|
2019
|
-
if ((item as AnthropicStreamEvent).type === "ping")
|
|
2043
|
+
if ((item as AnthropicStreamEvent).type === "ping") {
|
|
2044
|
+
if (!sawNonPingEvent) return false;
|
|
2045
|
+
if (pingProgressCapMs === undefined) return true;
|
|
2046
|
+
return Date.now() - lastNonPingProgressAtMs < pingProgressCapMs;
|
|
2047
|
+
}
|
|
2020
2048
|
sawNonPingEvent = true;
|
|
2049
|
+
lastNonPingProgressAtMs = Date.now();
|
|
2021
2050
|
return true;
|
|
2022
2051
|
},
|
|
2023
2052
|
});
|
|
@@ -2648,8 +2677,10 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2648
2677
|
thinkingDisplay,
|
|
2649
2678
|
isOAuth,
|
|
2650
2679
|
claudeCodeSessionId,
|
|
2680
|
+
disableStrictTools: disableStrictToolsOverride,
|
|
2651
2681
|
} = args;
|
|
2652
2682
|
const compat = model.compat;
|
|
2683
|
+
const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools;
|
|
2653
2684
|
const needsInterleavedBeta = interleavedThinking && !model.thinking?.supportsDisplay;
|
|
2654
2685
|
const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
|
|
2655
2686
|
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
@@ -2722,7 +2753,12 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2722
2753
|
isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
|
|
2723
2754
|
claudeCodeSessionId,
|
|
2724
2755
|
claudeCodeBetas: oauthToken
|
|
2725
|
-
? buildClaudeCodeBetas(
|
|
2756
|
+
? buildClaudeCodeBetas(
|
|
2757
|
+
hasTools || thinkingEnabled,
|
|
2758
|
+
thinkingEnabled,
|
|
2759
|
+
thinkingDisplay === "omitted",
|
|
2760
|
+
disableStrictTools,
|
|
2761
|
+
)
|
|
2726
2762
|
: [],
|
|
2727
2763
|
});
|
|
2728
2764
|
|
|
@@ -3529,6 +3565,40 @@ export function convertAnthropicMessages(
|
|
|
3529
3565
|
});
|
|
3530
3566
|
}
|
|
3531
3567
|
}
|
|
3568
|
+
// Anthropic's replay validator rejects any non-`tool_use` block that
|
|
3569
|
+
// appears after a `tool_use` inside an assistant turn (400:
|
|
3570
|
+
// "tool_use ids were found without tool_result blocks immediately
|
|
3571
|
+
// after: <id>"). A persisted turn can violate this when a mid-turn
|
|
3572
|
+
// server-side fallback handoff lands after the primary model already
|
|
3573
|
+
// emitted a tool_use — the replayed content is then e.g.
|
|
3574
|
+
// [thinking, text, tool_use, fallback, text, tool_use] — and also for
|
|
3575
|
+
// the older cross-provider [text, tool_use, text] shape (issue #544).
|
|
3576
|
+
// Stable-partition into [...non-tool_use, ...tool_use], preserving each
|
|
3577
|
+
// side's relative order: the non-tool_use chain (thinking → text →
|
|
3578
|
+
// fallback → text) carries thinking signatures and the fallback
|
|
3579
|
+
// boundary marker whose order Anthropic verifies, while tool_use blocks
|
|
3580
|
+
// are unsigned and safe to defer to the tail. Fast-path untouched when
|
|
3581
|
+
// already in order so prompt-cache prefixes stay byte-identical.
|
|
3582
|
+
let sawToolUse = false;
|
|
3583
|
+
let needsPartition = false;
|
|
3584
|
+
for (const block of blocks) {
|
|
3585
|
+
if (block.type === "tool_use") {
|
|
3586
|
+
sawToolUse = true;
|
|
3587
|
+
} else if (sawToolUse) {
|
|
3588
|
+
needsPartition = true;
|
|
3589
|
+
break;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
if (needsPartition) {
|
|
3593
|
+
const nonToolUse: ContentBlockParam[] = [];
|
|
3594
|
+
const toolUse: ContentBlockParam[] = [];
|
|
3595
|
+
for (const block of blocks) {
|
|
3596
|
+
if (block.type === "tool_use") toolUse.push(block);
|
|
3597
|
+
else nonToolUse.push(block);
|
|
3598
|
+
}
|
|
3599
|
+
blocks.length = 0;
|
|
3600
|
+
blocks.push(...nonToolUse, ...toolUse);
|
|
3601
|
+
}
|
|
3532
3602
|
if (blocks.length === 0) continue;
|
|
3533
3603
|
params.push({
|
|
3534
3604
|
role: "assistant",
|