@oh-my-pi/pi-ai 18.4.4 → 18.4.6
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 +23 -26
- package/dist/types/auth/cascade.d.ts +17 -6
- package/dist/types/auth/rank.d.ts +7 -1
- package/dist/types/auth/refresh.d.ts +8 -0
- package/dist/types/auth/types.d.ts +19 -2
- package/dist/types/auth-gateway/dispatch.d.ts +7 -7
- package/dist/types/auth-gateway/http.d.ts +4 -1
- package/dist/types/auth-gateway/types.d.ts +2 -0
- package/dist/types/auth-retry.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +7 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +4 -2
- package/dist/types/providers/cursor.d.ts +14 -12
- package/dist/types/providers/factory-droid/gemini.d.ts +15 -0
- package/dist/types/providers/factory-droid.d.ts +13 -0
- package/dist/types/providers/google-shared.d.ts +12 -0
- package/dist/types/providers/openai-chat-wire.d.ts +7 -2
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/registry/oauth/device-code.d.ts +2 -2
- package/dist/types/registry/oauth/factory-droid.d.ts +3 -0
- package/dist/types/registry/oauth/types.d.ts +13 -3
- package/dist/types/types.d.ts +5 -0
- package/dist/types/usage/factory-droid.d.ts +11 -0
- package/dist/types/usage.d.ts +4 -0
- package/dist/types/utils/schema/allowlist.d.ts +2 -0
- package/dist/types/utils/schema/index.d.ts +1 -0
- package/package.json +6 -6
- package/src/api-registry.ts +1 -0
- package/src/auth/cascade.ts +51 -11
- package/src/auth/health.ts +3 -0
- package/src/auth/oauth.ts +4 -0
- package/src/auth/rank.ts +11 -1
- package/src/auth/refresh.ts +27 -2
- package/src/auth/select.ts +52 -24
- package/src/auth/types.ts +18 -2
- package/src/auth/usage-cache.ts +6 -0
- package/src/auth/usage.ts +10 -5
- package/src/auth-gateway/dispatch.ts +20 -20
- package/src/auth-gateway/http.ts +44 -2
- package/src/auth-gateway/routes/embeddings.ts +1 -1
- package/src/auth-gateway/routes/images.ts +1 -1
- package/src/auth-gateway/routes/rerank.ts +1 -1
- package/src/auth-gateway/routes/speech.ts +1 -1
- package/src/auth-gateway/routes/systemone.ts +1 -1
- package/src/auth-gateway/routes/transcriptions.ts +1 -1
- package/src/auth-gateway/routes/video.ts +4 -3
- package/src/auth-gateway/server.ts +40 -10
- package/src/auth-gateway/types.ts +2 -0
- package/src/auth-retry.ts +3 -1
- package/src/auth-storage.ts +3 -1
- package/src/index.ts +1 -0
- package/src/providers/amazon-bedrock.ts +10 -8
- package/src/providers/anthropic.ts +126 -16
- package/src/providers/cursor/interaction-query.ts +11 -8
- package/src/providers/cursor.ts +980 -314
- package/src/providers/factory-droid/droid-identity.md +1 -0
- package/src/providers/factory-droid/gemini.ts +538 -0
- package/src/providers/factory-droid.ts +623 -0
- package/src/providers/google-shared.ts +2 -2
- package/src/providers/openai-chat-wire.ts +11 -2
- package/src/providers/openai-completions.ts +106 -13
- package/src/registry/engine/device-code.ts +2 -2
- package/src/registry/engine/refresh.ts +1 -0
- package/src/registry/hooks/device-code.ts +1 -0
- package/src/registry/oauth/device-code.ts +19 -3
- package/src/registry/oauth/factory-droid.ts +62 -0
- package/src/registry/oauth/types.ts +13 -3
- package/src/stream.ts +38 -4
- package/src/types.ts +5 -0
- package/src/usage/cursor.ts +3 -4
- package/src/usage/factory-droid.ts +226 -0
- package/src/usage/registry.ts +3 -0
- package/src/usage/xai-oauth.ts +21 -8
- package/src/usage.ts +4 -0
- package/src/utils/schema/CONSTRAINTS.md +15 -0
- package/src/utils/schema/allowlist.ts +160 -0
- package/src/utils/schema/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.4.
|
|
3
|
+
"version": "18.4.6",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.4.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
158
|
+
"@oh-my-pi/omptype": "18.4.6",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.4.6",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.4.6",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.4.6",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.4.6"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/api-registry.ts
CHANGED
package/src/auth/cascade.ts
CHANGED
|
@@ -8,7 +8,15 @@ import { getEnvApiKey, getEnvApiKeyName } from "../stream";
|
|
|
8
8
|
import type { SessionAffinity } from "./affinity";
|
|
9
9
|
import type { CredentialPool } from "./pool";
|
|
10
10
|
import type { CredentialSelector } from "./select";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AuthApiKeyOptions,
|
|
13
|
+
AuthCredential,
|
|
14
|
+
AuthSource,
|
|
15
|
+
AuthSourceOptions,
|
|
16
|
+
KeysApi,
|
|
17
|
+
LimitsApi,
|
|
18
|
+
OAuthRequestIdentity,
|
|
19
|
+
} from "./types";
|
|
12
20
|
|
|
13
21
|
/**
|
|
14
22
|
* Default config value resolver that checks env vars and treats as literal.
|
|
@@ -23,6 +31,7 @@ async function defaultConfigValueResolver(config: string): Promise<string | unde
|
|
|
23
31
|
export class KeyOverrides {
|
|
24
32
|
#runtimeOverrides: Map<string, string> = new Map();
|
|
25
33
|
#configOverrides: Map<string, string> = new Map();
|
|
34
|
+
#configFallbacks: Map<string, string> = new Map();
|
|
26
35
|
#configValueResolver: (config: string) => Promise<string | undefined>;
|
|
27
36
|
|
|
28
37
|
constructor(resolver?: (config: string) => Promise<string | undefined>) {
|
|
@@ -41,6 +50,11 @@ export class KeyOverrides {
|
|
|
41
50
|
return this.#configOverrides.get(provider);
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
/** Config value consulted only after stored OAuth/login credentials. */
|
|
54
|
+
fallbackKey(provider: string): string | undefined {
|
|
55
|
+
return this.#configFallbacks.get(provider);
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
/** Resolve a config value (env var name, "!command", literal) to the secret. */
|
|
45
59
|
resolve(config: string): Promise<string | undefined> {
|
|
46
60
|
return this.#configValueResolver(config);
|
|
@@ -70,16 +84,28 @@ export class KeyOverrides {
|
|
|
70
84
|
*
|
|
71
85
|
* Lower priority than {@link KeyOverrides.setRuntime} so a CLI `--api-key`
|
|
72
86
|
* still wins for the duration of a single invocation.
|
|
87
|
+
*
|
|
88
|
+
* `fallback: true` instead ranks the value below stored OAuth and `/login`
|
|
89
|
+
* credentials (at the env-var tier). Providers that own a `/login` flow use
|
|
90
|
+
* this so their default key reference (e.g. an unset env-var name, which
|
|
91
|
+
* resolves to its literal text) cannot shadow the key the user logged in with.
|
|
73
92
|
*/
|
|
74
|
-
setConfig(provider: string, apiKeyConfig: string): void {
|
|
75
|
-
|
|
93
|
+
setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void {
|
|
94
|
+
if (options?.fallback) {
|
|
95
|
+
this.#configOverrides.delete(provider);
|
|
96
|
+
this.#configFallbacks.set(provider, apiKeyConfig);
|
|
97
|
+
} else {
|
|
98
|
+
this.#configFallbacks.delete(provider);
|
|
99
|
+
this.#configOverrides.set(provider, apiKeyConfig);
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
/**
|
|
79
|
-
* Remove a single config-sourced API key override.
|
|
104
|
+
* Remove a single config-sourced API key (override or fallback).
|
|
80
105
|
*/
|
|
81
106
|
removeConfig(provider: string): void {
|
|
82
107
|
this.#configOverrides.delete(provider);
|
|
108
|
+
this.#configFallbacks.delete(provider);
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
/**
|
|
@@ -88,6 +114,7 @@ export class KeyOverrides {
|
|
|
88
114
|
*/
|
|
89
115
|
clearConfig(): void {
|
|
90
116
|
this.#configOverrides.clear();
|
|
117
|
+
this.#configFallbacks.clear();
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
/**
|
|
@@ -169,7 +196,7 @@ export class KeyCascade implements KeysApi {
|
|
|
169
196
|
/**
|
|
170
197
|
* Classify where a provider's auth comes from, following the same precedence
|
|
171
198
|
* as {@link KeyCascade.get}: runtime override → config override →
|
|
172
|
-
* stored OAuth → login-stored api_key → env var → stored api_key.
|
|
199
|
+
* stored OAuth → login-stored api_key → config fallback → env var → stored api_key.
|
|
173
200
|
* Returns undefined when no auth is configured.
|
|
174
201
|
*
|
|
175
202
|
* Compact, structured counterpart to {@link KeyCascade.describe}; `env`
|
|
@@ -184,6 +211,7 @@ export class KeyCascade implements KeysApi {
|
|
|
184
211
|
if (bearing.some(credential => credential.type === "api_key" && credential.source === "login")) {
|
|
185
212
|
return { kind: "api_key", concrete: true };
|
|
186
213
|
}
|
|
214
|
+
if (this.#deps.overrides.fallbackKey(provider) !== undefined) return { kind: "config", concrete: true };
|
|
187
215
|
if (
|
|
188
216
|
(env === "dedicated" && this.#hasDedicatedEnvAuth(provider)) ||
|
|
189
217
|
(env === "aliases" && (this.#hasDedicatedEnvAuth(provider) || Boolean(getEnvApiKey(provider))))
|
|
@@ -256,6 +284,9 @@ export class KeyCascade implements KeysApi {
|
|
|
256
284
|
return this.#deps.overrides.resolve(loginApiKeySelection.credential.key);
|
|
257
285
|
}
|
|
258
286
|
|
|
287
|
+
const fallbackKey = this.#deps.overrides.fallbackKey(provider);
|
|
288
|
+
if (fallbackKey !== undefined) return this.#deps.overrides.resolve(fallbackKey);
|
|
289
|
+
|
|
259
290
|
const envKey = getEnvApiKey(provider);
|
|
260
291
|
if (envKey) return envKey;
|
|
261
292
|
|
|
@@ -273,10 +304,12 @@ export class KeyCascade implements KeysApi {
|
|
|
273
304
|
options?: AuthApiKeyOptions,
|
|
274
305
|
): Promise<ResolvedApiKey | undefined> {
|
|
275
306
|
let credentialId: number | undefined;
|
|
276
|
-
|
|
307
|
+
let oauthIdentity: OAuthRequestIdentity | undefined;
|
|
308
|
+
const apiKey = await this.get(provider, sessionId, options, (id, identity) => {
|
|
277
309
|
credentialId = id;
|
|
310
|
+
oauthIdentity = identity;
|
|
278
311
|
});
|
|
279
|
-
return apiKey === undefined ? undefined : { apiKey, credentialId };
|
|
312
|
+
return apiKey === undefined ? undefined : { apiKey, credentialId, ...(oauthIdentity ? { oauthIdentity } : {}) };
|
|
280
313
|
}
|
|
281
314
|
|
|
282
315
|
/**
|
|
@@ -288,7 +321,7 @@ export class KeyCascade implements KeysApi {
|
|
|
288
321
|
provider: string,
|
|
289
322
|
sessionId?: string,
|
|
290
323
|
options?: AuthApiKeyOptions,
|
|
291
|
-
onCredentialId?: (id: number) => void,
|
|
324
|
+
onCredentialId?: (id: number, identity?: OAuthRequestIdentity) => void,
|
|
292
325
|
): Promise<string | undefined> {
|
|
293
326
|
// Runtime override takes highest priority
|
|
294
327
|
const runtimeKey = this.#deps.overrides.runtimeKey(provider);
|
|
@@ -310,7 +343,10 @@ export class KeyCascade implements KeysApi {
|
|
|
310
343
|
// then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
|
|
311
344
|
const oauthResolved = await this.#deps.selector.resolveOAuth(provider, sessionId, options);
|
|
312
345
|
if (oauthResolved) {
|
|
313
|
-
if (oauthResolved.credentialId !== undefined)
|
|
346
|
+
if (onCredentialId && oauthResolved.credentialId !== undefined) {
|
|
347
|
+
const { orgId, region, inferenceRegion } = oauthResolved.credential;
|
|
348
|
+
onCredentialId(oauthResolved.credentialId, { orgId, region, inferenceRegion });
|
|
349
|
+
}
|
|
314
350
|
return oauthResolved.apiKey;
|
|
315
351
|
}
|
|
316
352
|
const loginApiKeySelection = await this.#deps.selector.selectApiKey(
|
|
@@ -334,6 +370,9 @@ export class KeyCascade implements KeysApi {
|
|
|
334
370
|
// suppresses account_uuid for this session.
|
|
335
371
|
if (sessionId) this.#deps.affinity.forget(provider, sessionId);
|
|
336
372
|
|
|
373
|
+
const fallbackKey = this.#deps.overrides.fallbackKey(provider);
|
|
374
|
+
if (fallbackKey !== undefined) return this.#deps.overrides.resolve(fallbackKey);
|
|
375
|
+
|
|
337
376
|
const envKey = getEnvApiKey(provider);
|
|
338
377
|
if (envKey) return envKey;
|
|
339
378
|
const apiKeySelection = await this.#deps.selector.selectApiKey(
|
|
@@ -458,6 +497,7 @@ export class KeyCascade implements KeysApi {
|
|
|
458
497
|
!this.isKeylessFallback(provider, credential),
|
|
459
498
|
);
|
|
460
499
|
if (loginApiKeySource) return loginApiKeySource;
|
|
500
|
+
if (this.#deps.overrides.fallbackKey(provider) !== undefined) return "provider config (fallback)";
|
|
461
501
|
if (getEnvApiKey(provider)) return `env (over ${baseLabel})`;
|
|
462
502
|
const apiKeySource = describeStored(
|
|
463
503
|
"api_key",
|
|
@@ -475,8 +515,8 @@ export class KeyCascade implements KeysApi {
|
|
|
475
515
|
this.#deps.overrides.removeRuntime(provider);
|
|
476
516
|
}
|
|
477
517
|
|
|
478
|
-
setConfig(provider: string, apiKeyConfig: string): void {
|
|
479
|
-
this.#deps.overrides.setConfig(provider, apiKeyConfig);
|
|
518
|
+
setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void {
|
|
519
|
+
this.#deps.overrides.setConfig(provider, apiKeyConfig, options);
|
|
480
520
|
}
|
|
481
521
|
|
|
482
522
|
removeConfig(provider: string): void {
|
package/src/auth/health.ts
CHANGED
|
@@ -76,6 +76,9 @@ export class CredentialHealth implements HealthApi {
|
|
|
76
76
|
email: credential.email,
|
|
77
77
|
enterpriseUrl: credential.enterpriseUrl,
|
|
78
78
|
apiEndpoint: credential.apiEndpoint,
|
|
79
|
+
orgId: credential.orgId,
|
|
80
|
+
region: credential.region,
|
|
81
|
+
inferenceRegion: credential.inferenceRegion,
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
|
package/src/auth/oauth.ts
CHANGED
|
@@ -151,6 +151,8 @@ export class OAuthAccounts implements OAuthApi {
|
|
|
151
151
|
apiEndpoint: credential.apiEndpoint,
|
|
152
152
|
orgId: credential.orgId,
|
|
153
153
|
orgName: credential.orgName,
|
|
154
|
+
region: credential.region,
|
|
155
|
+
inferenceRegion: credential.inferenceRegion,
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -206,6 +208,8 @@ export class OAuthAccounts implements OAuthApi {
|
|
|
206
208
|
enterpriseUrl: credential.enterpriseUrl,
|
|
207
209
|
orgId: credential.orgId,
|
|
208
210
|
orgName: credential.orgName,
|
|
211
|
+
region: credential.region,
|
|
212
|
+
inferenceRegion: credential.inferenceRegion,
|
|
209
213
|
};
|
|
210
214
|
} catch (error) {
|
|
211
215
|
return {
|
package/src/auth/rank.ts
CHANGED
|
@@ -20,6 +20,11 @@ export type UsageCandidate<T extends AuthCredential> = {
|
|
|
20
20
|
inReserve?: boolean;
|
|
21
21
|
/** True only when reserve ranking had a usable remaining-fraction measurement. */
|
|
22
22
|
reserveMeasured?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Present after ranking: the account's renewable allowance for this request is
|
|
25
|
+
* spent, so if it still serves it draws on paid overage such as Codex credits.
|
|
26
|
+
*/
|
|
27
|
+
allowanceSpent?: boolean;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
/** OAuth credential eligible for usage ranking. */
|
|
@@ -39,6 +44,7 @@ export type UsageRankedCandidate<T extends AuthCredential> = UsageCandidate<T> &
|
|
|
39
44
|
reserveMeasured?: boolean;
|
|
40
45
|
accountPriority: number;
|
|
41
46
|
hasPriorityBoost: boolean;
|
|
47
|
+
allowanceSpent: boolean;
|
|
42
48
|
usageMeasured: boolean;
|
|
43
49
|
planPriority: number;
|
|
44
50
|
secondaryUsed: number;
|
|
@@ -90,6 +96,9 @@ function compareUsageRankedCandidatePriority(
|
|
|
90
96
|
if (planGated && left.planPriority !== right.planPriority) {
|
|
91
97
|
return left.planPriority - right.planPriority;
|
|
92
98
|
}
|
|
99
|
+
// Paid overage (Codex credits) never renews, so an account serving past its
|
|
100
|
+
// allowance yields to any sibling whose renewable allowance is left (#13889).
|
|
101
|
+
if (left.allowanceSpent !== right.allowanceSpent) return left.allowanceSpent ? 1 : -1;
|
|
93
102
|
if (left.inReserve !== right.inReserve) return left.inReserve ? 1 : -1;
|
|
94
103
|
if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
|
|
95
104
|
// Short-window guard: candidates whose primary (e.g. 5h) window is
|
|
@@ -130,7 +139,7 @@ function compareUsageRankedCandidates(
|
|
|
130
139
|
return priority !== 0 ? priority : left.orderPos - right.orderPos;
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
/** Sort ranked candidates by blocks, plan, reserve, boost, hot window, usage and drain. */
|
|
142
|
+
/** Sort ranked candidates by blocks, plan, spent allowance, reserve, boost, hot window, usage and drain. */
|
|
134
143
|
export function orderUsageRankedCandidates<T extends AuthCredential>(
|
|
135
144
|
candidates: UsageRankedCandidate<T>[],
|
|
136
145
|
planGated: boolean,
|
|
@@ -142,5 +151,6 @@ export function orderUsageRankedCandidates<T extends AuthCredential>(
|
|
|
142
151
|
usageChecked: candidate.usageChecked,
|
|
143
152
|
inReserve: candidate.inReserve,
|
|
144
153
|
reserveMeasured: candidate.reserveMeasured,
|
|
154
|
+
allowanceSpent: candidate.allowanceSpent,
|
|
145
155
|
}));
|
|
146
156
|
}
|
package/src/auth/refresh.ts
CHANGED
|
@@ -44,6 +44,32 @@ const OAUTH_REFRESH_LEASE_RENEW_MS = 5_000;
|
|
|
44
44
|
const OAUTH_REFRESH_OPERATION_TIMEOUT_MS = 10_000;
|
|
45
45
|
const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
46
46
|
|
|
47
|
+
type OrganizationScope = Pick<
|
|
48
|
+
OAuthCredentials,
|
|
49
|
+
"orgId" | "region" | "inferenceRegion" | "activeOrganizationId" | "orgName"
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Organization-scoped identity after a refresh: refreshed values win; stored
|
|
54
|
+
* org/residency/inference scope carries over only while the organization is
|
|
55
|
+
* unchanged. The WorkOS selection always falls back to the stored one.
|
|
56
|
+
*/
|
|
57
|
+
export function mergeRefreshedOrganizationScope(
|
|
58
|
+
current: OrganizationScope,
|
|
59
|
+
refreshed: OrganizationScope,
|
|
60
|
+
): OrganizationScope {
|
|
61
|
+
const sameOrg =
|
|
62
|
+
(refreshed.orgId === undefined || refreshed.orgId === current.orgId) &&
|
|
63
|
+
(refreshed.activeOrganizationId === undefined || refreshed.activeOrganizationId === current.activeOrganizationId);
|
|
64
|
+
return {
|
|
65
|
+
orgId: refreshed.orgId ?? (sameOrg ? current.orgId : undefined),
|
|
66
|
+
region: refreshed.region ?? (sameOrg ? current.region : undefined),
|
|
67
|
+
inferenceRegion: refreshed.inferenceRegion ?? (sameOrg ? current.inferenceRegion : undefined),
|
|
68
|
+
activeOrganizationId: refreshed.activeOrganizationId ?? current.activeOrganizationId,
|
|
69
|
+
orgName: refreshed.orgName ?? (sameOrg ? current.orgName : undefined),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
/** Merge provider refresh bytes with the stored OAuth row, preserving subtype metadata for every refresh path. */
|
|
48
74
|
export function mergeRefreshedCredential<T extends OAuthCredential>(current: T, refreshed: OAuthCredentials): T {
|
|
49
75
|
return {
|
|
@@ -56,8 +82,7 @@ export function mergeRefreshedCredential<T extends OAuthCredential>(current: T,
|
|
|
56
82
|
projectId: refreshed.projectId ?? current.projectId,
|
|
57
83
|
enterpriseUrl: refreshed.enterpriseUrl ?? current.enterpriseUrl,
|
|
58
84
|
apiEndpoint: refreshed.apiEndpoint ?? current.apiEndpoint,
|
|
59
|
-
|
|
60
|
-
orgName: refreshed.orgName ?? current.orgName,
|
|
85
|
+
...mergeRefreshedOrganizationScope(current, refreshed),
|
|
61
86
|
authorizedAt: refreshed.authorizedAt ?? current.authorizedAt,
|
|
62
87
|
};
|
|
63
88
|
}
|
package/src/auth/select.ts
CHANGED
|
@@ -267,6 +267,7 @@ export class CredentialSelector {
|
|
|
267
267
|
blockedUntil,
|
|
268
268
|
inReserve: false,
|
|
269
269
|
accountPriority: 0,
|
|
270
|
+
allowanceSpent: remainingUsageFraction(strategy, usage, args.rankingContext, nowMs) === 0,
|
|
270
271
|
usageMeasured,
|
|
271
272
|
hasPriorityBoost: strategy.hasPriorityBoost?.(primary, primaryUncapped, args.rankingContext) ?? false,
|
|
272
273
|
planPriority: 0,
|
|
@@ -469,6 +470,7 @@ export class CredentialSelector {
|
|
|
469
470
|
reserveFraction !== undefined && remainingFraction !== undefined && remainingFraction <= reserveFraction,
|
|
470
471
|
reserveMeasured: reserveFraction !== undefined && remainingFraction !== undefined,
|
|
471
472
|
accountPriority: policy?.priority === undefined || !Number.isFinite(policy.priority) ? 0 : policy.priority,
|
|
473
|
+
allowanceSpent: remainingFraction === 0,
|
|
472
474
|
usageMeasured,
|
|
473
475
|
hasPriorityBoost: strategy?.hasPriorityBoost?.(primary, primaryUncapped, args.rankingContext) ?? false,
|
|
474
476
|
planPriority: planPriority(args.planGate, usage),
|
|
@@ -573,12 +575,27 @@ export class CredentialSelector {
|
|
|
573
575
|
sessionPreferredCanRefreshOrUse &&
|
|
574
576
|
!this.#deps.blocks.isBlocked(provider, providerKey, sessionPreferredIndex, blockScopes);
|
|
575
577
|
const sessionPinIsExplicit = sessionCredential?.type === "oauth" && sessionCredential.explicit === true;
|
|
578
|
+
const rankDespitePin =
|
|
579
|
+
!sessionPreferredIsAvailable ||
|
|
580
|
+
!sessionPreferredIsWarm ||
|
|
581
|
+
hasPlanRequirement ||
|
|
582
|
+
(policyReserveEnabled && !sessionPinIsExplicit);
|
|
583
|
+
// A warm automatic pin whose allowance is spent keeps serving on paid overage
|
|
584
|
+
// (Codex credits) and is never blocked, so check it and rank when spent: a
|
|
585
|
+
// sibling with renewable allowance left must take over (#13889). tryOAuth
|
|
586
|
+
// reads this same report for the pin, so it is handed on below.
|
|
587
|
+
const sessionPreferredUsage =
|
|
588
|
+
checkUsage && !rankDespitePin && !sessionPinIsExplicit && sessionPreferredCredential
|
|
589
|
+
? await this.#deps.usage.report(provider, sessionPreferredCredential, {
|
|
590
|
+
...options,
|
|
591
|
+
timeoutMs: this.#deps.usage.requestTimeoutMs,
|
|
592
|
+
})
|
|
593
|
+
: undefined;
|
|
576
594
|
const shouldRank =
|
|
577
595
|
checkUsage &&
|
|
578
|
-
(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
(policyReserveEnabled && !sessionPinIsExplicit));
|
|
596
|
+
(rankDespitePin ||
|
|
597
|
+
(sessionPreferredUsage !== undefined &&
|
|
598
|
+
remainingUsageFraction(strategy, sessionPreferredUsage, rankingContext, Date.now()) === 0));
|
|
582
599
|
// When ranking, seed the pinned credential first in the evaluation order so it wins genuine
|
|
583
600
|
// ties (the ranked comparator falls back to `orderPos`) without overriding a strictly-better
|
|
584
601
|
// sibling — this respects the residual value of a same-account shared static prefix that other
|
|
@@ -621,11 +638,11 @@ export class CredentialSelector {
|
|
|
621
638
|
: policyOrder
|
|
622
639
|
.map(idx => credentials[idx])
|
|
623
640
|
.filter((selection): selection is { credential: OAuthCredential; index: number } => Boolean(selection))
|
|
624
|
-
.map(selection =>
|
|
625
|
-
selection
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
641
|
+
.map(selection =>
|
|
642
|
+
selection.index === sessionPreferredIndex && sessionPreferredUsage !== undefined
|
|
643
|
+
? { selection, usage: sessionPreferredUsage, usageChecked: true }
|
|
644
|
+
: { selection, usage: null, usageChecked: false },
|
|
645
|
+
);
|
|
629
646
|
const preflightFailures = new Set<OAuthCandidate>();
|
|
630
647
|
|
|
631
648
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
@@ -634,23 +651,34 @@ export class CredentialSelector {
|
|
|
634
651
|
candidate.selection.index === sessionPreferredIndex,
|
|
635
652
|
);
|
|
636
653
|
const preferredCandidate = sessionPreferredCandidate === -1 ? undefined : candidates[sessionPreferredCandidate];
|
|
637
|
-
|
|
654
|
+
// A warm automatic pin normally wins. Two policies may evict it, each only
|
|
655
|
+
// while a sibling is confirmed better: reserve (sibling measured outside
|
|
656
|
+
// reserve) and spent allowance (unblocked sibling with allowance left).
|
|
657
|
+
const automaticPinWouldBeEvicted = (excludePreflightFailures: boolean): boolean =>
|
|
638
658
|
!sessionPinIsExplicit &&
|
|
639
|
-
preferredCandidate
|
|
640
|
-
candidates.some(
|
|
641
|
-
candidate
|
|
642
|
-
|
|
643
|
-
|
|
659
|
+
preferredCandidate !== undefined &&
|
|
660
|
+
candidates.some(candidate => {
|
|
661
|
+
if (candidate === preferredCandidate) return false;
|
|
662
|
+
if (excludePreflightFailures && preflightFailures.has(candidate)) return false;
|
|
663
|
+
if (
|
|
664
|
+
preferredCandidate.inReserve === true &&
|
|
644
665
|
candidate.reserveMeasured === true &&
|
|
645
|
-
candidate.inReserve === false
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
666
|
+
candidate.inReserve === false
|
|
667
|
+
) {
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
return (
|
|
671
|
+
preferredCandidate.allowanceSpent === true &&
|
|
672
|
+
candidate.usage !== null &&
|
|
673
|
+
candidate.allowanceSpent === false &&
|
|
674
|
+
!this.#deps.blocks.isBlocked(provider, providerKey, candidate.selection.index, blockScopes)
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
const pinEvictedBeforePreflight = automaticPinWouldBeEvicted(false);
|
|
650
678
|
if (
|
|
651
679
|
!hasPlanRequirement &&
|
|
652
680
|
sessionPreferredCandidate > 0 &&
|
|
653
|
-
(!shouldRank || sessionPinIsExplicit || (sessionPreferredIsWarm && !
|
|
681
|
+
(!shouldRank || sessionPinIsExplicit || (sessionPreferredIsWarm && !pinEvictedBeforePreflight))
|
|
654
682
|
) {
|
|
655
683
|
const [preferred] = candidates.splice(sessionPreferredCandidate, 1);
|
|
656
684
|
candidates.unshift(preferred);
|
|
@@ -774,14 +802,14 @@ export class CredentialSelector {
|
|
|
774
802
|
}),
|
|
775
803
|
);
|
|
776
804
|
|
|
777
|
-
const
|
|
805
|
+
const pinEvictedAfterPreflight = automaticPinWouldBeEvicted(true);
|
|
778
806
|
if (
|
|
779
807
|
!hasPlanRequirement &&
|
|
780
808
|
preferredCandidate !== undefined &&
|
|
781
809
|
!preflightFailures.has(preferredCandidate) &&
|
|
782
810
|
sessionPreferredIsWarm &&
|
|
783
811
|
!sessionPinIsExplicit &&
|
|
784
|
-
!
|
|
812
|
+
!pinEvictedAfterPreflight
|
|
785
813
|
) {
|
|
786
814
|
const preferredIndex = candidates.indexOf(preferredCandidate);
|
|
787
815
|
if (preferredIndex > 0) {
|
|
@@ -803,7 +831,7 @@ export class CredentialSelector {
|
|
|
803
831
|
// unenforced and the pin is not known-ineligible) so an active session never
|
|
804
832
|
// silently migrates accounts mid-conversation; blocked, exhausted, or
|
|
805
833
|
// known-ineligible pins still fall through to the ranked sibling.
|
|
806
|
-
if (hasPlanRequirement && sessionPreferredCandidate > 0 && !
|
|
834
|
+
if (hasPlanRequirement && sessionPreferredCandidate > 0 && !pinEvictedAfterPreflight) {
|
|
807
835
|
const preferred = candidates[sessionPreferredCandidate]!;
|
|
808
836
|
const planEligibility = planGate?.(preferred.usage);
|
|
809
837
|
if (planEligibility === true || (!enforcePlanRequirement && planEligibility !== false)) {
|
package/src/auth/types.ts
CHANGED
|
@@ -214,6 +214,9 @@ export type CompletionProbeCredential =
|
|
|
214
214
|
email?: string;
|
|
215
215
|
enterpriseUrl?: string;
|
|
216
216
|
apiEndpoint?: string;
|
|
217
|
+
orgId?: string;
|
|
218
|
+
region?: string;
|
|
219
|
+
inferenceRegion?: "global" | "eu" | "us";
|
|
217
220
|
};
|
|
218
221
|
|
|
219
222
|
/**
|
|
@@ -486,6 +489,13 @@ export type AuthApiKeyOptions = {
|
|
|
486
489
|
refreshReason?: OAuthRefreshReason;
|
|
487
490
|
};
|
|
488
491
|
|
|
492
|
+
/** Non-secret identity bound to the OAuth credential selected for one request attempt. */
|
|
493
|
+
export interface OAuthRequestIdentity {
|
|
494
|
+
orgId?: string;
|
|
495
|
+
region?: string;
|
|
496
|
+
inferenceRegion?: "global" | "eu" | "us";
|
|
497
|
+
}
|
|
498
|
+
|
|
489
499
|
/**
|
|
490
500
|
* Refreshed OAuth access plus identity metadata returned by
|
|
491
501
|
* {@link AuthStorage.oauth.access}. Callers that authenticate via a bearer
|
|
@@ -505,6 +515,8 @@ export interface OAuthAccess {
|
|
|
505
515
|
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
506
516
|
orgId?: string;
|
|
507
517
|
orgName?: string;
|
|
518
|
+
region?: string;
|
|
519
|
+
inferenceRegion?: "global" | "eu" | "us";
|
|
508
520
|
}
|
|
509
521
|
|
|
510
522
|
/**
|
|
@@ -933,10 +945,14 @@ export interface KeysApi {
|
|
|
933
945
|
*
|
|
934
946
|
* Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
|
|
935
947
|
* still wins for the duration of a single invocation.
|
|
948
|
+
*
|
|
949
|
+
* `fallback: true` ranks the value below stored OAuth and `/login`
|
|
950
|
+
* credentials instead, so a provider's default key reference cannot shadow
|
|
951
|
+
* a key the user logged in with.
|
|
936
952
|
*/
|
|
937
|
-
setConfig(provider: string, apiKeyConfig: string): void;
|
|
953
|
+
setConfig(provider: string, apiKeyConfig: string, options?: { fallback?: boolean }): void;
|
|
938
954
|
/**
|
|
939
|
-
* Remove a single config-sourced API key override.
|
|
955
|
+
* Remove a single config-sourced API key (override or fallback).
|
|
940
956
|
*/
|
|
941
957
|
removeConfig(provider: string): void;
|
|
942
958
|
/**
|
package/src/auth/usage-cache.ts
CHANGED
|
@@ -65,6 +65,9 @@ export function buildUsageCredential(credential: AuthCredential): UsageCredentia
|
|
|
65
65
|
orgName: credential.orgName,
|
|
66
66
|
enterpriseUrl: credential.enterpriseUrl,
|
|
67
67
|
apiEndpoint: credential.apiEndpoint,
|
|
68
|
+
region: credential.region,
|
|
69
|
+
inferenceRegion: credential.inferenceRegion,
|
|
70
|
+
activeOrganizationId: credential.activeOrganizationId,
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -81,6 +84,9 @@ export function usageCacheIdentity(credential: UsageCredential): string {
|
|
|
81
84
|
if (projectId) parts.push(`project:${projectId}`);
|
|
82
85
|
const enterpriseUrl = credential.enterpriseUrl?.trim().toLowerCase();
|
|
83
86
|
if (enterpriseUrl) parts.push(`enterprise:${enterpriseUrl}`);
|
|
87
|
+
if (credential.region) parts.push(`region:${credential.region}`);
|
|
88
|
+
if (credential.inferenceRegion) parts.push(`inference:${credential.inferenceRegion}`);
|
|
89
|
+
if (credential.activeOrganizationId) parts.push(`selected-org:${credential.activeOrganizationId}`);
|
|
84
90
|
// Only fall back to a secret-derived key when a stable account identifier is
|
|
85
91
|
// unavailable. Including the token hash when accountId/email/orgId are present
|
|
86
92
|
// causes cache misses on every OAuth refresh — usage data is per-account (or
|
package/src/auth/usage.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import type { UsageCache, UsageRequestDescriptor } from "./usage-cache";
|
|
35
35
|
import type { CredentialPool } from "./pool";
|
|
36
36
|
import type { RankingStrategyResolver } from "../usage/registry";
|
|
37
|
-
import { OAUTH_REFRESH_SKEW_MS } from "./refresh";
|
|
37
|
+
import { mergeRefreshedOrganizationScope, OAUTH_REFRESH_SKEW_MS } from "./refresh";
|
|
38
38
|
import type { OAuthRefresher } from "./refresh";
|
|
39
39
|
import { USAGE_REPORT_TTL_MS } from "./sqlite-credential-store";
|
|
40
40
|
import type { AuthCredentialStore } from "./store";
|
|
@@ -64,6 +64,9 @@ export function buildRefreshableOauthCredential(credential: UsageCredential): OA
|
|
|
64
64
|
orgName: credential.orgName,
|
|
65
65
|
enterpriseUrl: credential.enterpriseUrl,
|
|
66
66
|
apiEndpoint: credential.apiEndpoint,
|
|
67
|
+
region: credential.region,
|
|
68
|
+
inferenceRegion: credential.inferenceRegion,
|
|
69
|
+
activeOrganizationId: credential.activeOrganizationId,
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -82,8 +85,7 @@ export function mergeRefreshedUsageCredential(
|
|
|
82
85
|
email: refreshed.email ?? credential.email,
|
|
83
86
|
enterpriseUrl: refreshed.enterpriseUrl ?? credential.enterpriseUrl,
|
|
84
87
|
apiEndpoint: refreshed.apiEndpoint ?? credential.apiEndpoint,
|
|
85
|
-
|
|
86
|
-
orgName: refreshed.orgName ?? credential.orgName,
|
|
88
|
+
...mergeRefreshedOrganizationScope(credential, refreshed),
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -189,8 +191,11 @@ export class UsageService implements UsageApi {
|
|
|
189
191
|
email: next.email ?? entry.credential.email,
|
|
190
192
|
enterpriseUrl: next.enterpriseUrl ?? entry.credential.enterpriseUrl,
|
|
191
193
|
apiEndpoint: next.apiEndpoint ?? entry.credential.apiEndpoint,
|
|
192
|
-
orgId: next.orgId
|
|
193
|
-
orgName: next.orgName
|
|
194
|
+
orgId: next.orgId,
|
|
195
|
+
orgName: next.orgName,
|
|
196
|
+
region: next.region,
|
|
197
|
+
inferenceRegion: next.inferenceRegion,
|
|
198
|
+
activeOrganizationId: next.activeOrganizationId,
|
|
194
199
|
});
|
|
195
200
|
}
|
|
196
201
|
|