@linxiraos/pi-ai 1.1.26 → 1.2.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/CHANGELOG.md +9 -8
- package/README.md +15 -0
- package/dist/types/auth/affinity.d.ts +33 -6
- package/dist/types/auth/cascade.d.ts +44 -7
- package/dist/types/auth/oauth.d.ts +9 -6
- package/dist/types/auth/policy.d.ts +1 -0
- package/dist/types/auth/refresh.d.ts +1 -2
- package/dist/types/auth/select.d.ts +6 -4
- package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
- package/dist/types/auth/store.d.ts +8 -2
- package/dist/types/auth/types.d.ts +99 -15
- package/dist/types/auth-broker/client.d.ts +3 -3
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -4
- package/dist/types/auth-broker/server.d.ts +2 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/dispatch.d.ts +13 -5
- package/dist/types/auth-gateway/http.d.ts +0 -2
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
- package/dist/types/auth-gateway/routes/images.d.ts +3 -3
- package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
- package/dist/types/auth-gateway/routes/video.d.ts +4 -4
- package/dist/types/auth-gateway/server.d.ts +22 -5
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/stdio.d.ts +12 -0
- package/dist/types/auth-storage.d.ts +7 -6
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -0
- package/dist/types/error/auth.d.ts +10 -0
- package/dist/types/error/flags.d.ts +38 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/images/shared.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/judgment/index.d.ts +2 -1
- package/dist/types/judgment/native.d.ts +80 -0
- package/dist/types/judgment/openai-decisions.d.ts +3 -0
- package/dist/types/judgment/question-file.d.ts +52 -0
- package/dist/types/judgment/systemone.d.ts +7 -0
- package/dist/types/judgment/types.d.ts +2 -2
- package/dist/types/judgment/wire.d.ts +36 -0
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-client.d.ts +10 -0
- package/dist/types/providers/anthropic-state.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +38 -3
- package/dist/types/providers/aws-credentials.d.ts +4 -8
- package/dist/types/providers/connect-error-detail.d.ts +9 -0
- package/dist/types/providers/connect-frame.d.ts +44 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
- package/dist/types/providers/cursor.d.ts +8 -8
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/google-shared.d.ts +24 -4
- package/dist/types/providers/inference-headers.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-configuration-update.d.ts +20 -11
- package/dist/types/providers/openai-responses-wire.d.ts +1 -55
- package/dist/types/providers/openai-responses.d.ts +40 -3
- package/dist/types/providers/openai-shared.d.ts +30 -5
- package/dist/types/providers/pi-native-client.d.ts +1 -1
- package/dist/types/providers/transform-messages.d.ts +25 -0
- package/dist/types/providers/vision-guard.d.ts +1 -1
- package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
- package/dist/types/registry/oauth/cursor.d.ts +10 -2
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/types.d.ts +116 -14
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +21 -7
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/dist/types/utils/block-symbols.d.ts +14 -0
- package/dist/types/utils/dsml-leak.d.ts +29 -0
- package/dist/types/utils/glyph-codec.d.ts +1 -1
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
- package/dist/types/utils/resolve-peer.d.ts +2 -0
- package/dist/types/utils/retry-wait.d.ts +9 -0
- package/dist/types/utils/schema/normalize.d.ts +8 -3
- package/dist/types/utils/stream-markup-healing.d.ts +4 -4
- package/dist/types/utils/tool-call-arguments.d.ts +8 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +130 -37
- package/src/auth/blocks.ts +94 -15
- package/src/auth/cascade.ts +112 -17
- package/src/auth/health.ts +3 -2
- package/src/auth/oauth.ts +25 -10
- package/src/auth/policy.ts +10 -1
- package/src/auth/pool.ts +0 -5
- package/src/auth/refresh.ts +17 -12
- package/src/auth/resets.ts +1 -1
- package/src/auth/rotation.ts +8 -2
- package/src/auth/select.ts +102 -30
- package/src/auth/sqlite-credential-store.ts +119 -49
- package/src/auth/store.ts +8 -2
- package/src/auth/types.ts +108 -17
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +2 -2
- package/src/auth-broker/client.ts +7 -4
- package/src/auth-broker/discover.ts +6 -5
- package/src/auth-broker/refresher.ts +8 -1
- package/src/auth-broker/remote-store.ts +200 -62
- package/src/auth-broker/server.ts +316 -130
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/dispatch.ts +15 -7
- package/src/auth-gateway/http.ts +0 -8
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +2 -2
- package/src/auth-gateway/routes/images.ts +4 -4
- package/src/auth-gateway/routes/rerank.ts +2 -2
- package/src/auth-gateway/routes/speech.ts +2 -2
- package/src/auth-gateway/routes/systemone.ts +22 -14
- package/src/auth-gateway/routes/transcriptions.ts +2 -2
- package/src/auth-gateway/routes/video.ts +7 -7
- package/src/auth-gateway/server.ts +175 -134
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/auth-gateway/stdio.ts +132 -0
- package/src/auth-storage.ts +23 -9
- package/src/dialect/deepseek.ts +67 -8
- package/src/dialect/gemini.ts +8 -2
- package/src/dialect/hermes.ts +8 -2
- package/src/dialect/kimi.ts +8 -2
- package/src/dialect/qwen-xml.ts +8 -2
- package/src/dialect/qwen3.ts +8 -2
- package/src/dialect/terminator-wait.ts +42 -0
- package/src/env-api-key.ts +78 -0
- package/src/error/auth.ts +20 -0
- package/src/error/flags.ts +143 -24
- package/src/error/rate-limit.ts +8 -6
- package/src/images/google-antigravity.ts +0 -1
- package/src/images/index.ts +3 -17
- package/src/images/openai-hosted.ts +13 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +2 -1
- package/src/judgment/index.ts +2 -1
- package/src/judgment/native.ts +333 -0
- package/src/judgment/openai-decisions-predicate.md +9 -0
- package/src/judgment/openai-decisions.ts +129 -0
- package/src/judgment/question-file.ts +90 -0
- package/src/judgment/systemone.ts +65 -0
- package/src/judgment/text.ts +9 -1
- package/src/judgment/types.ts +2 -2
- package/src/judgment/wire.ts +42 -0
- package/src/provider-details.ts +1 -1
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +19 -8
- package/src/providers/anthropic-client.ts +16 -1
- package/src/providers/anthropic-state.ts +6 -0
- package/src/providers/anthropic.ts +962 -238
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/aws-credentials.ts +65 -5
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-error-detail.ts +21 -0
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +590 -133
- package/src/providers/devin.ts +68 -58
- package/src/providers/factory-droid/gemini.ts +38 -51
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/google-gemini-cli.ts +4 -0
- package/src/providers/google-shared.ts +134 -105
- package/src/providers/google-vertex.ts +0 -1
- package/src/providers/google.ts +1 -1
- package/src/providers/inference-headers.ts +19 -0
- package/src/providers/mock.ts +4 -0
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex/request-transformer.ts +16 -11
- package/src/providers/openai-codex-responses.ts +201 -76
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-configuration-update.ts +54 -15
- package/src/providers/openai-responses-wire.ts +1 -55
- package/src/providers/openai-responses.ts +437 -33
- package/src/providers/openai-shared.ts +267 -72
- package/src/providers/pi-native-client.ts +2 -2
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +1 -1
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +240 -84
- package/src/providers/vision-guard.ts +1 -1
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +26 -3
- package/src/registry/hooks/api-key.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/callback-server.ts +6 -1
- package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
- package/src/registry/oauth/cursor.ts +80 -17
- package/src/registry/oauth/index.ts +5 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +85 -107
- package/src/transcription/types.ts +2 -1
- package/src/types.ts +142 -15
- package/src/usage/claude.ts +19 -12
- package/src/usage/commandcode.ts +1 -1
- package/src/usage/cursor.ts +14 -25
- package/src/usage/github-copilot.ts +1 -1
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +8 -1
- package/src/usage.ts +8 -0
- package/src/utils/anthropic-auth.ts +34 -10
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/block-symbols.ts +17 -0
- package/src/utils/dsml-leak.ts +109 -0
- package/src/utils/empty-completion-retry.ts +2 -3
- package/src/utils/glyph-codec.ts +200 -7
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/leaked-thinking-stream.ts +47 -25
- package/src/utils/resolve-peer.ts +7 -0
- package/src/utils/retry-wait.ts +19 -0
- package/src/utils/schema/CONSTRAINTS.md +9 -7
- package/src/utils/schema/fields.ts +20 -0
- package/src/utils/schema/normalize.ts +65 -23
- package/src/utils/schema/wire.ts +3 -3
- package/src/utils/stream-markup-healing.ts +4 -4
- package/src/utils/thinking-loop.ts +36 -13
- package/src/utils/tool-call-arguments.ts +16 -0
- package/src/utils/transport-fetch.ts +1 -1
- package/src/utils.ts +41 -10
- package/dist/types/judgment/typesafe.d.ts +0 -49
- package/src/judgment/typesafe.ts +0 -183
package/src/auth/refresh.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { OAuthCredentials, OAuthProvider } from "../registry/oauth/types";
|
|
|
5
5
|
import type { Provider } from "../types";
|
|
6
6
|
import { raceSignal } from "./abort";
|
|
7
7
|
import { authCredentialEquals, type CredentialPool, credentialDisabledEvent } from "./pool";
|
|
8
|
-
import type { AccountPolicies } from "./policy";
|
|
9
8
|
import { resolveCredentialIdentityKey, serializeCredential } from "./sqlite-credential-store";
|
|
10
9
|
import { hasRefreshLeases, type AuthCredentialStore } from "./store";
|
|
11
10
|
import {
|
|
@@ -91,8 +90,8 @@ export function mergeRefreshedCredential<T extends OAuthCredential>(current: T,
|
|
|
91
90
|
export interface OAuthRefresherDeps {
|
|
92
91
|
store: AuthCredentialStore;
|
|
93
92
|
pool: CredentialPool;
|
|
94
|
-
policies: AccountPolicies;
|
|
95
93
|
override?: AuthStorageOptions["refreshOAuthCredential"];
|
|
94
|
+
overrideMints?: AuthStorageOptions["refreshOAuthCredentialMints"];
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
/** Single-flighted, lease-guarded OAuth refresh with compare-and-set persistence. */
|
|
@@ -139,6 +138,13 @@ export class OAuthRefresher {
|
|
|
139
138
|
this.#recentMints.set(id, { provider, access: credential.access, at: now });
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
/** Whether refreshes run the provider token exchange in this process rather than delegating it. */
|
|
142
|
+
#mintsLocally(): boolean {
|
|
143
|
+
return this.#deps.override
|
|
144
|
+
? this.#deps.overrideMints === true
|
|
145
|
+
: this.#deps.store.refreshOAuthCredential === undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
/**
|
|
143
149
|
* Refresh one stored OAuth credential under durable row ownership.
|
|
144
150
|
*/
|
|
@@ -345,7 +351,7 @@ export class OAuthRefresher {
|
|
|
345
351
|
} else {
|
|
346
352
|
this.#deps.store.updateAuthCredential(row.id, merged);
|
|
347
353
|
}
|
|
348
|
-
if (this.#
|
|
354
|
+
if (this.#mintsLocally()) {
|
|
349
355
|
this.#rememberMint(provider, row.id, merged);
|
|
350
356
|
}
|
|
351
357
|
this.#deps.pool.replace(
|
|
@@ -422,7 +428,6 @@ export class OAuthRefresher {
|
|
|
422
428
|
await this.#deps.pool.reload();
|
|
423
429
|
return "cas-lost";
|
|
424
430
|
}
|
|
425
|
-
this.#deps.policies.validateFor(provider, this.#deps.pool.credentials(provider));
|
|
426
431
|
return "disabled";
|
|
427
432
|
}
|
|
428
433
|
|
|
@@ -462,11 +467,7 @@ export class OAuthRefresher {
|
|
|
462
467
|
.then(refreshed => {
|
|
463
468
|
// A delegated refresh may have returned a broker-cached token rather
|
|
464
469
|
// than minting one. Only direct local requests can establish mint time.
|
|
465
|
-
if (
|
|
466
|
-
!hasRefreshLeases(this.#deps.store) &&
|
|
467
|
-
this.#deps.override === undefined &&
|
|
468
|
-
this.#deps.store.refreshOAuthCredential === undefined
|
|
469
|
-
) {
|
|
470
|
+
if (!hasRefreshLeases(this.#deps.store) && this.#mintsLocally()) {
|
|
470
471
|
this.#rememberMint(provider, credentialId, refreshed);
|
|
471
472
|
}
|
|
472
473
|
return refreshed;
|
|
@@ -613,7 +614,7 @@ export class OAuthRefresher {
|
|
|
613
614
|
const promise = (async () => {
|
|
614
615
|
this.#deps.pool.bump("credential-refresh-start");
|
|
615
616
|
try {
|
|
616
|
-
return await this.#forceRefreshCredentialByIdUnshared(id, signal);
|
|
617
|
+
return await this.#forceRefreshCredentialByIdUnshared(id, signal, options?.reason);
|
|
617
618
|
} catch (error) {
|
|
618
619
|
this.#deps.pool.bump("credential-refresh-failure");
|
|
619
620
|
throw error;
|
|
@@ -625,7 +626,11 @@ export class OAuthRefresher {
|
|
|
625
626
|
return raceSignal(promise, signal, "credential refresh aborted");
|
|
626
627
|
}
|
|
627
628
|
|
|
628
|
-
async #forceRefreshCredentialByIdUnshared(
|
|
629
|
+
async #forceRefreshCredentialByIdUnshared(
|
|
630
|
+
id: number,
|
|
631
|
+
signal?: AbortSignal,
|
|
632
|
+
reason?: OAuthRefreshReason,
|
|
633
|
+
): Promise<AuthCredentialSnapshotEntry> {
|
|
629
634
|
for (const provider of this.#deps.pool.providers()) {
|
|
630
635
|
const entries = this.#deps.pool.entries(provider);
|
|
631
636
|
const index = entries.findIndex(entry => entry.id === id);
|
|
@@ -645,7 +650,7 @@ export class OAuthRefresher {
|
|
|
645
650
|
const stale: OAuthCredential = { ...attempted, expires: 0 };
|
|
646
651
|
let refreshed: OAuthCredentials;
|
|
647
652
|
try {
|
|
648
|
-
refreshed = await this.#refreshSingleFlight(provider as Provider, stale, id, signal);
|
|
653
|
+
refreshed = await this.#refreshSingleFlight(provider as Provider, stale, id, signal, reason);
|
|
649
654
|
} catch (error) {
|
|
650
655
|
// A definitively-dead grant tears the row down here, where the
|
|
651
656
|
// attempted credential is known. CAS on the persisted credential so a
|
package/src/auth/resets.ts
CHANGED
|
@@ -262,7 +262,7 @@ export class ResetCredits implements ResetsApi {
|
|
|
262
262
|
resetSpentCredentialId: access.credentialId,
|
|
263
263
|
});
|
|
264
264
|
if (this.#deps.store.invalidateUsageCache) {
|
|
265
|
-
await this.#deps.store.invalidateUsageCache(options.signal).catch(err => {
|
|
265
|
+
await this.#deps.store.invalidateUsageCache(provider, options.signal).catch(err => {
|
|
266
266
|
logger.debug("Failed to notify store of stale usage", { err });
|
|
267
267
|
});
|
|
268
268
|
}
|
package/src/auth/rotation.ts
CHANGED
|
@@ -141,6 +141,7 @@ export class RateLimits implements LimitsApi {
|
|
|
141
141
|
}
|
|
142
142
|
#blockCredentialForRotation(
|
|
143
143
|
provider: string,
|
|
144
|
+
sessionId: string | undefined,
|
|
144
145
|
credentialType: AuthCredential["type"],
|
|
145
146
|
targetIndex: number,
|
|
146
147
|
blockedUntil: number,
|
|
@@ -188,7 +189,9 @@ export class RateLimits implements LimitsApi {
|
|
|
188
189
|
.map((credential, index) => ({ credential, index }))
|
|
189
190
|
.filter(
|
|
190
191
|
(entry): entry is { credential: AuthCredential; index: number } =>
|
|
191
|
-
entry.credential.type === credentialType &&
|
|
192
|
+
entry.credential.type === credentialType &&
|
|
193
|
+
entry.index !== targetIndex &&
|
|
194
|
+
this.#deps.affinity.allows(provider, sessionId, entry.credential),
|
|
192
195
|
);
|
|
193
196
|
|
|
194
197
|
let retryAtMs: number | undefined;
|
|
@@ -292,6 +295,7 @@ export class RateLimits implements LimitsApi {
|
|
|
292
295
|
.findIndex(entry => entry.id === targetCredentialId && entry.credential.type === credentialType);
|
|
293
296
|
const rotation = this.#blockCredentialForRotation(
|
|
294
297
|
provider,
|
|
298
|
+
sessionId,
|
|
295
299
|
credentialType,
|
|
296
300
|
targetIndex,
|
|
297
301
|
blockedUntil,
|
|
@@ -456,6 +460,7 @@ export class RateLimits implements LimitsApi {
|
|
|
456
460
|
}
|
|
457
461
|
const mark = this.#blockCredentialForRotation(
|
|
458
462
|
provider,
|
|
463
|
+
sessionId,
|
|
459
464
|
sessionCredential.type,
|
|
460
465
|
sessionCredential.index,
|
|
461
466
|
Date.now() + DEFAULT_BLOCK_MS,
|
|
@@ -474,7 +479,8 @@ export class RateLimits implements LimitsApi {
|
|
|
474
479
|
(credential, index) =>
|
|
475
480
|
credential.type === sessionCredential.type &&
|
|
476
481
|
index !== sessionCredential.index &&
|
|
477
|
-
!this.#deps.blocks.isBlocked(provider, providerKey, index)
|
|
482
|
+
!this.#deps.blocks.isBlocked(provider, providerKey, index) &&
|
|
483
|
+
this.#deps.affinity.allows(provider, sessionId, credential),
|
|
478
484
|
);
|
|
479
485
|
const target = this.#deps.pool.entries(provider)[sessionCredential.index];
|
|
480
486
|
const sticky = this.#deps.affinity.get(provider, sessionId);
|
package/src/auth/select.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getOAuthApiKey, getOAuthProvider } from "../registry/oauth";
|
|
|
4
4
|
import type { OAuthCredentials, OAuthProvider } from "../registry/oauth/types";
|
|
5
5
|
import type { Provider } from "../types";
|
|
6
6
|
import type { CredentialRankingContext, CredentialRankingStrategy, PlanGate, UsageReport } from "../usage";
|
|
7
|
+
import { isWithinUsageReserve } from "../usage";
|
|
7
8
|
import type { RankingStrategyResolver } from "../usage/registry";
|
|
8
9
|
import type { SessionAffinity } from "./affinity";
|
|
9
10
|
import {
|
|
@@ -28,7 +29,14 @@ import {
|
|
|
28
29
|
} from "./rank";
|
|
29
30
|
import { mergeRefreshedCredential, OAUTH_REFRESH_SKEW_MS, type OAuthRefresher } from "./refresh";
|
|
30
31
|
import type { AuthCredentialStore } from "./store";
|
|
31
|
-
import
|
|
32
|
+
import {
|
|
33
|
+
type ApiKeyCredential,
|
|
34
|
+
type AuthApiKeyOptions,
|
|
35
|
+
type AuthCredential,
|
|
36
|
+
type OAuthCredential,
|
|
37
|
+
oauthAccountKey,
|
|
38
|
+
type StoredAuthCredential,
|
|
39
|
+
} from "./types";
|
|
32
40
|
import type { UsageService } from "./usage";
|
|
33
41
|
import {
|
|
34
42
|
isUsageLimitReached,
|
|
@@ -59,6 +67,8 @@ export type TryOAuthOptions = {
|
|
|
59
67
|
blockScopes?: readonly string[];
|
|
60
68
|
/** When false, a definitive failure of THIS credential returns undefined instead of falling back to the ranked/round-robin selector (target-only resolution). */
|
|
61
69
|
allowFallback?: boolean;
|
|
70
|
+
/** Receives a non-definitive refresh failure that left this credential unusable; the caller filters for retryable ones. */
|
|
71
|
+
onTransientRefreshFailure?: (error: unknown) => void;
|
|
62
72
|
};
|
|
63
73
|
|
|
64
74
|
/** Services consulted by CredentialSelector for policy, usage, blocks, refresh, and session affinity. */
|
|
@@ -467,7 +477,9 @@ export class CredentialSelector {
|
|
|
467
477
|
blocked,
|
|
468
478
|
blockedUntil,
|
|
469
479
|
inReserve:
|
|
470
|
-
reserveFraction !== undefined &&
|
|
480
|
+
reserveFraction !== undefined &&
|
|
481
|
+
remainingFraction !== undefined &&
|
|
482
|
+
isWithinUsageReserve(remainingFraction, reserveFraction),
|
|
471
483
|
reserveMeasured: reserveFraction !== undefined && remainingFraction !== undefined,
|
|
472
484
|
accountPriority: policy?.priority === undefined || !Number.isFinite(policy.priority) ? 0 : policy.priority,
|
|
473
485
|
allowanceSpent: remainingFraction === 0,
|
|
@@ -493,9 +505,9 @@ export class CredentialSelector {
|
|
|
493
505
|
* 1. strict: unblocked credentials only, usage limits respected, plan
|
|
494
506
|
* filter enforced (when any account is confirmed eligible);
|
|
495
507
|
* 2. plan-fitting last resort: same plan filter, but blocked/exhausted
|
|
496
|
-
* accounts are allowed (
|
|
497
|
-
* first) so the caller gets real
|
|
498
|
-
* instead of a missing key;
|
|
508
|
+
* accounts are allowed (an explicit session pin first, then blocked
|
|
509
|
+
* candidates earliest-unblocking first) so the caller gets real
|
|
510
|
+
* usage-limit semantics from the wire instead of a missing key;
|
|
499
511
|
* 3. unfiltered last resort: the plan filter matched nothing usable —
|
|
500
512
|
* skip it and try every account once; the server is the final arbiter
|
|
501
513
|
* of model access.
|
|
@@ -510,16 +522,22 @@ export class CredentialSelector {
|
|
|
510
522
|
options?: AuthApiKeyOptions,
|
|
511
523
|
): Promise<OAuthResolutionResult | undefined> {
|
|
512
524
|
await this.#deps.pool.adoptExternalChanges();
|
|
513
|
-
const
|
|
525
|
+
const stored = this.#deps.pool
|
|
514
526
|
.credentials(provider)
|
|
515
527
|
.map((credential, index) => ({ credential, index }))
|
|
516
528
|
.filter((entry): entry is { credential: OAuthCredential; index: number } => entry.credential.type === "oauth");
|
|
517
529
|
this.#deps.policies.validateFor(
|
|
518
530
|
provider,
|
|
519
|
-
|
|
531
|
+
stored.map(entry => entry.credential),
|
|
520
532
|
);
|
|
533
|
+
// A session restriction drops every other account before ranking, pins,
|
|
534
|
+
// and the fallback passes below, so none of them can route back to it.
|
|
535
|
+
const credentials = stored.filter(entry => this.#deps.affinity.allows(provider, sessionId, entry.credential));
|
|
521
536
|
|
|
522
537
|
if (credentials.length === 0) return undefined;
|
|
538
|
+
// Ranking and preflight both await provider work; preserve the row each
|
|
539
|
+
// selection started with even if a peer reorders the live pool.
|
|
540
|
+
const credentialIds = this.#deps.pool.entries(provider).map(entry => entry.id);
|
|
523
541
|
this.#deps.policies.validateUsageCapability(provider, this.#deps.usage.canFetchOAuthUsage(provider));
|
|
524
542
|
|
|
525
543
|
const providerKey = providerTypeKey(provider, "oauth");
|
|
@@ -536,9 +554,10 @@ export class CredentialSelector {
|
|
|
536
554
|
const accountIds = options?.accountIds?.length ? new Set(options.accountIds) : undefined;
|
|
537
555
|
const enforceAccounts =
|
|
538
556
|
accountIds !== undefined &&
|
|
539
|
-
credentials.some(
|
|
540
|
-
|
|
541
|
-
|
|
557
|
+
credentials.some(({ credential }) => {
|
|
558
|
+
const accountKey = oauthAccountKey(credential);
|
|
559
|
+
return accountKey !== undefined && accountIds.has(accountKey);
|
|
560
|
+
});
|
|
542
561
|
const hasAccountPolicy = credentials.some(
|
|
543
562
|
({ credential }) => this.#deps.policies.forCredential(provider, credential) !== undefined,
|
|
544
563
|
);
|
|
@@ -643,7 +662,20 @@ export class CredentialSelector {
|
|
|
643
662
|
? { selection, usage: sessionPreferredUsage, usageChecked: true }
|
|
644
663
|
: { selection, usage: null, usageChecked: false },
|
|
645
664
|
);
|
|
665
|
+
// Keep the candidate object: preflight may rebind its positional index
|
|
666
|
+
// after a peer changes the credential pool.
|
|
667
|
+
const explicitPin = sessionPinIsExplicit
|
|
668
|
+
? candidates.find(candidate => candidate.selection.index === sessionPreferredIndex)
|
|
669
|
+
: undefined;
|
|
646
670
|
const preflightFailures = new Set<OAuthCandidate>();
|
|
671
|
+
// The last retryable refresh error (network, timeout, 5xx) that removed a candidate.
|
|
672
|
+
// When no candidate resolves, it is rethrown so callers retry instead of reporting
|
|
673
|
+
// a missing key. Refresher outcomes after a dead grant (row disabled, CAS lost) are
|
|
674
|
+
// classified auth failures and keep resolving to undefined.
|
|
675
|
+
let transientRefreshFailure: unknown;
|
|
676
|
+
const recordTransientRefreshFailure = (error: unknown): void => {
|
|
677
|
+
if (AIError.retriable(AIError.classify(error))) transientRefreshFailure = error;
|
|
678
|
+
};
|
|
647
679
|
|
|
648
680
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
649
681
|
candidate =>
|
|
@@ -690,16 +722,24 @@ export class CredentialSelector {
|
|
|
690
722
|
const forceRefreshIndex = options?.forceRefresh
|
|
691
723
|
? (sessionPreferredIndex ?? candidates[0]?.selection.index)
|
|
692
724
|
: undefined;
|
|
725
|
+
// Candidate IDs were captured before ranking. Their synchronous rebinds
|
|
726
|
+
// share one provider re-list; resyncs after refresh await re-read the store.
|
|
727
|
+
let preflightRows: StoredAuthCredential[] | undefined;
|
|
693
728
|
await Promise.all(
|
|
694
729
|
candidates.map(async candidate => {
|
|
695
730
|
const force = forceRefreshIndex !== undefined && candidate.selection.index === forceRefreshIndex;
|
|
696
|
-
const initialCredentialId =
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
731
|
+
const initialCredentialId = credentialIds[candidate.selection.index];
|
|
732
|
+
if (initialCredentialId === undefined) {
|
|
733
|
+
preflightFailures.add(candidate);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const beforeSync = candidate.selection.credential;
|
|
737
|
+
preflightRows ??= this.#reloadProviderRows(provider);
|
|
738
|
+
if (!this.#rebindOAuthSelection(preflightRows, candidate.selection, initialCredentialId)) {
|
|
739
|
+
preflightFailures.add(candidate);
|
|
740
|
+
return;
|
|
702
741
|
}
|
|
742
|
+
const syncedPeerCredential = !authCredentialEquals(beforeSync, candidate.selection.credential);
|
|
703
743
|
const hasFreshAccess = Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires;
|
|
704
744
|
if ((!force || syncedPeerCredential) && hasFreshAccess) return;
|
|
705
745
|
const latestCredential = this.#deps.pool.credentials(provider)[candidate.selection.index];
|
|
@@ -785,16 +825,21 @@ export class CredentialSelector {
|
|
|
785
825
|
// to a sibling account with the wrong prefetched usage/plan.
|
|
786
826
|
return;
|
|
787
827
|
}
|
|
788
|
-
} else
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
this.#deps.
|
|
792
|
-
provider
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
828
|
+
} else {
|
|
829
|
+
recordTransientRefreshFailure(error);
|
|
830
|
+
if (credentialId !== undefined) {
|
|
831
|
+
const latestIndex = this.#deps.pool
|
|
832
|
+
.entries(provider)
|
|
833
|
+
.findIndex(entry => entry.id === credentialId);
|
|
834
|
+
if (latestIndex !== -1) {
|
|
835
|
+
this.#deps.blocks.mark(
|
|
836
|
+
provider,
|
|
837
|
+
providerKey,
|
|
838
|
+
latestIndex,
|
|
839
|
+
Date.now() + OAUTH_REFRESH_FAILURE_BACKOFF_MS,
|
|
840
|
+
AUTH_BLOCK_SCOPE,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
798
843
|
}
|
|
799
844
|
}
|
|
800
845
|
preflightFailures.add(candidate);
|
|
@@ -850,12 +895,18 @@ export class CredentialSelector {
|
|
|
850
895
|
];
|
|
851
896
|
if (enforcePlanRequirement) passes.push({ allowBlocked: true, enforcePlanRequirement: false, enforceAccounts });
|
|
852
897
|
if (enforceAccounts) passes.push({ allowBlocked: true, enforcePlanRequirement: false, enforceAccounts: false });
|
|
898
|
+
// Blocked candidates rank earliest-unblock first, which would route a
|
|
899
|
+
// blocked explicit pin to an equally blocked sibling. Once the strict
|
|
900
|
+
// pass finds no unblocked account, the user's pin goes first.
|
|
901
|
+
const lastResortCandidates = explicitPin
|
|
902
|
+
? [explicitPin, ...candidates.filter(candidate => candidate !== explicitPin)]
|
|
903
|
+
: candidates;
|
|
853
904
|
|
|
854
905
|
for (const pass of passes) {
|
|
855
|
-
for (const candidate of candidates) {
|
|
906
|
+
for (const candidate of pass.allowBlocked ? lastResortCandidates : candidates) {
|
|
856
907
|
if (preflightFailures.has(candidate)) continue;
|
|
857
|
-
const
|
|
858
|
-
if (pass.enforceAccounts && (
|
|
908
|
+
const candidateAccountKey = oauthAccountKey(candidate.selection.credential);
|
|
909
|
+
if (pass.enforceAccounts && (candidateAccountKey === undefined || !accountIds?.has(candidateAccountKey)))
|
|
859
910
|
continue;
|
|
860
911
|
const resolved = await this.tryOAuth(provider, candidate.selection, providerKey, sessionId, options, {
|
|
861
912
|
checkUsage,
|
|
@@ -868,11 +919,15 @@ export class CredentialSelector {
|
|
|
868
919
|
rankingContext,
|
|
869
920
|
blockScope,
|
|
870
921
|
blockScopes,
|
|
922
|
+
onTransientRefreshFailure: recordTransientRefreshFailure,
|
|
871
923
|
});
|
|
872
924
|
if (resolved) return resolved;
|
|
873
925
|
}
|
|
874
926
|
}
|
|
875
927
|
|
|
928
|
+
if (transientRefreshFailure !== undefined) {
|
|
929
|
+
throw new AIError.OAuthRefreshUnavailableError(provider, transientRefreshFailure);
|
|
930
|
+
}
|
|
876
931
|
return undefined;
|
|
877
932
|
}
|
|
878
933
|
|
|
@@ -881,11 +936,25 @@ export class CredentialSelector {
|
|
|
881
936
|
selection: { credential: OAuthCredential; index: number },
|
|
882
937
|
credentialId: number,
|
|
883
938
|
): boolean {
|
|
939
|
+
return this.#rebindOAuthSelection(this.#reloadProviderRows(provider), selection, credentialId);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Re-list one provider's active rows and adopt them in the pool. */
|
|
943
|
+
#reloadProviderRows(provider: string): StoredAuthCredential[] {
|
|
884
944
|
const latestRows = this.#deps.store.listAuthCredentials(provider);
|
|
885
945
|
this.#deps.pool.replace(
|
|
886
946
|
provider,
|
|
887
947
|
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
888
948
|
);
|
|
949
|
+
return latestRows;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Point `selection` at row `credentialId` in `latestRows`; false when it is gone or no longer OAuth. */
|
|
953
|
+
#rebindOAuthSelection(
|
|
954
|
+
latestRows: readonly StoredAuthCredential[],
|
|
955
|
+
selection: { credential: OAuthCredential; index: number },
|
|
956
|
+
credentialId: number,
|
|
957
|
+
): boolean {
|
|
889
958
|
const latestIndex = latestRows.findIndex(row => row.id === credentialId);
|
|
890
959
|
if (latestIndex === -1) return false;
|
|
891
960
|
const latest = latestRows[latestIndex];
|
|
@@ -1055,7 +1124,9 @@ export class CredentialSelector {
|
|
|
1055
1124
|
}
|
|
1056
1125
|
}
|
|
1057
1126
|
this.#deps.pool.noteBearer(provider, result.apiKey, credentialId);
|
|
1058
|
-
|
|
1127
|
+
if (options?.recordAffinity !== false) {
|
|
1128
|
+
this.#deps.affinity.record(provider, sessionId, "oauth", selection.index);
|
|
1129
|
+
}
|
|
1059
1130
|
return { apiKey: result.apiKey, credential: updated, credentialId };
|
|
1060
1131
|
} catch (error) {
|
|
1061
1132
|
const errorMsg = String(error);
|
|
@@ -1087,6 +1158,7 @@ export class CredentialSelector {
|
|
|
1087
1158
|
if (allowFallback) return this.resolveOAuth(provider, sessionId, options);
|
|
1088
1159
|
}
|
|
1089
1160
|
} else {
|
|
1161
|
+
usageOptions.onTransientRefreshFailure?.(error);
|
|
1090
1162
|
// Block temporarily for transient failures (5 minutes)
|
|
1091
1163
|
this.#deps.blocks.mark(
|
|
1092
1164
|
provider,
|