@oh-my-pi/pi-ai 17.0.8 → 17.1.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 +37 -0
- package/README.md +3 -0
- package/dist/types/auth-broker/discover.d.ts +4 -0
- package/dist/types/auth-broker/remote-store.d.ts +14 -2
- package/dist/types/auth-storage.d.ts +45 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/providers/google-shared.d.ts +11 -0
- package/dist/types/providers/openai-chat-wire.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +3 -0
- package/dist/types/providers/openai-responses-wire.d.ts +17 -0
- package/dist/types/providers/openai-responses.d.ts +8 -2
- package/dist/types/providers/openai-shared.d.ts +32 -11
- package/dist/types/providers/transform-messages.d.ts +7 -0
- package/dist/types/registry/alibaba-token-plan.d.ts +7 -0
- package/dist/types/registry/meta.d.ts +7 -0
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +12 -0
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +35 -1
- package/dist/types/registry/oauth/xai-oauth.d.ts +25 -9
- package/dist/types/registry/oauth/zai.d.ts +25 -0
- package/dist/types/registry/registry.d.ts +15 -0
- package/dist/types/registry/zai.d.ts +8 -0
- package/dist/types/types.d.ts +27 -0
- package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
- package/dist/types/usage/synthetic.d.ts +2 -0
- package/dist/types/usage/xai-oauth.d.ts +12 -0
- package/dist/types/usage.d.ts +11 -0
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +56 -1
- package/src/auth-broker/remote-store.ts +93 -13
- package/src/auth-storage.ts +258 -15
- package/src/index.ts +2 -0
- package/src/providers/__tests__/kimi-code-thinking.test.ts +44 -0
- package/src/providers/amazon-bedrock.ts +54 -37
- package/src/providers/azure-openai-responses.ts +6 -0
- package/src/providers/gitlab-duo.ts +4 -0
- package/src/providers/google-shared.ts +30 -0
- package/src/providers/openai-anthropic-shim.ts +2 -0
- package/src/providers/openai-chat-server.ts +30 -0
- package/src/providers/openai-chat-wire.ts +12 -0
- package/src/providers/openai-completions.ts +70 -2
- package/src/providers/openai-responses-server.ts +22 -0
- package/src/providers/openai-responses-wire.ts +8 -0
- package/src/providers/openai-responses.ts +227 -8
- package/src/providers/openai-shared.ts +121 -11
- package/src/providers/pi-native-client.ts +9 -0
- package/src/providers/pi-native-server.ts +3 -0
- package/src/providers/transform-messages.ts +64 -23
- package/src/registry/alibaba-token-plan.ts +43 -0
- package/src/registry/meta.ts +22 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +214 -5
- package/src/registry/oauth/callback-server.ts +19 -12
- package/src/registry/oauth/google-antigravity.ts +34 -18
- package/src/registry/oauth/google-gemini-cli.ts +56 -32
- package/src/registry/oauth/google-oauth-shared.ts +107 -21
- package/src/registry/oauth/xai-oauth.ts +146 -17
- package/src/registry/oauth/zai.ts +285 -0
- package/src/registry/registry.ts +6 -1
- package/src/registry/zai.ts +14 -0
- package/src/stream.ts +53 -1
- package/src/types.ts +28 -0
- package/src/usage/alibaba-token-plan.ts +202 -0
- package/src/usage/claude.ts +125 -1
- package/src/usage/synthetic.ts +180 -0
- package/src/usage/xai-oauth.ts +414 -0
- package/src/usage/zai.ts +9 -3
- package/src/usage.ts +9 -0
|
@@ -14,6 +14,10 @@ declare const ALL: ({
|
|
|
14
14
|
readonly name: "Alibaba Coding Plan";
|
|
15
15
|
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<import("./oauth/index.js").OAuthCredentials>;
|
|
16
16
|
readonly getApiKey: (credentials: import("./oauth/index.js").OAuthCredentials) => string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly id: "alibaba-token-plan";
|
|
19
|
+
readonly name: "QwenCloud Token Plan";
|
|
20
|
+
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
|
|
17
21
|
} | {
|
|
18
22
|
readonly id: "amazon-bedrock";
|
|
19
23
|
readonly name: "Amazon Bedrock";
|
|
@@ -143,6 +147,10 @@ declare const ALL: ({
|
|
|
143
147
|
readonly id: "lm-studio";
|
|
144
148
|
readonly name: "LM Studio (Local OpenAI-compatible)";
|
|
145
149
|
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
|
|
150
|
+
} | {
|
|
151
|
+
readonly id: "meta";
|
|
152
|
+
readonly name: "Meta Model API";
|
|
153
|
+
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
|
|
146
154
|
} | {
|
|
147
155
|
readonly id: "minimax";
|
|
148
156
|
readonly name: "MiniMax";
|
|
@@ -292,6 +300,13 @@ declare const ALL: ({
|
|
|
292
300
|
readonly id: "zai";
|
|
293
301
|
readonly name: "Z.AI (GLM Coding Plan)";
|
|
294
302
|
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
|
|
303
|
+
} | {
|
|
304
|
+
readonly id: "zai-coding-plan";
|
|
305
|
+
readonly name: "Z.AI (GLM Coding Plan · Sign in)";
|
|
306
|
+
readonly storeCredentialsAs: "zai";
|
|
307
|
+
readonly callbackPort: 54548;
|
|
308
|
+
readonly pasteCodeFlow: true;
|
|
309
|
+
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string | import("./oauth/index.js").OAuthCredentials>;
|
|
295
310
|
} | {
|
|
296
311
|
readonly id: "zenmux";
|
|
297
312
|
readonly name: "ZenMux";
|
|
@@ -5,3 +5,11 @@ export declare const zaiProvider: {
|
|
|
5
5
|
readonly name: "Z.AI (GLM Coding Plan)";
|
|
6
6
|
readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
|
|
7
7
|
};
|
|
8
|
+
export declare const zaiCodingPlanProvider: {
|
|
9
|
+
readonly id: "zai-coding-plan";
|
|
10
|
+
readonly name: "Z.AI (GLM Coding Plan · Sign in)";
|
|
11
|
+
readonly storeCredentialsAs: "zai";
|
|
12
|
+
readonly callbackPort: 54548;
|
|
13
|
+
readonly pasteCodeFlow: true;
|
|
14
|
+
readonly login: (cb: OAuthLoginCallbacks) => Promise<string | import("./oauth/index.js").OAuthCredentials>;
|
|
15
|
+
};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -188,6 +188,15 @@ export interface CodexCompactionMetadata {
|
|
|
188
188
|
export interface CodexCompactionRequestContext extends CodexCompactionMetadata {
|
|
189
189
|
operationId: string;
|
|
190
190
|
}
|
|
191
|
+
/** OpenAI's GPT-5.6+ explicit prompt-cache controls. */
|
|
192
|
+
export interface OpenAIPromptCacheOptions {
|
|
193
|
+
/** `explicit` disables OpenAI's automatic latest-message breakpoint. */
|
|
194
|
+
mode: "implicit" | "explicit";
|
|
195
|
+
/** The only currently supported minimum breakpoint lifetime. */
|
|
196
|
+
ttl?: "30m";
|
|
197
|
+
/** By default, mark one existing block from stable history; `none` suppresses that marker. */
|
|
198
|
+
breakpoint?: "latest-stable-message" | "none";
|
|
199
|
+
}
|
|
191
200
|
export interface StreamOptions {
|
|
192
201
|
temperature?: number;
|
|
193
202
|
topP?: number;
|
|
@@ -258,6 +267,18 @@ export interface StreamOptions {
|
|
|
258
267
|
* `x-grok-conv-id`; when omitted, they fall back to `sessionId`.
|
|
259
268
|
*/
|
|
260
269
|
promptCacheKey?: string;
|
|
270
|
+
/**
|
|
271
|
+
* OpenAI GPT-5.6+ prompt-cache policy. Ignored by providers that do not
|
|
272
|
+
* support explicit OpenAI cache breakpoints; explicit mode fails locally on
|
|
273
|
+
* incompatible OpenAI-compatible endpoints.
|
|
274
|
+
*/
|
|
275
|
+
promptCache?: OpenAIPromptCacheOptions;
|
|
276
|
+
/**
|
|
277
|
+
* Disable OpenAI Responses server-side turn chaining for this request.
|
|
278
|
+
* Diagnostic callers that compare independent requests can set this to
|
|
279
|
+
* `false` so `previous_response_id` cannot explain a result.
|
|
280
|
+
*/
|
|
281
|
+
statefulResponses?: boolean;
|
|
261
282
|
/**
|
|
262
283
|
* Provider-scoped mutable state store for this agent session.
|
|
263
284
|
* Providers can use this to persist transport/session state between turns.
|
|
@@ -393,6 +414,12 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
|
393
414
|
* or the catalog entry already names the variant).
|
|
394
415
|
*/
|
|
395
416
|
openrouterVariant?: string;
|
|
417
|
+
/**
|
|
418
|
+
* Caller-owned Google context-cache resource name. Forwarded only to the
|
|
419
|
+
* direct Gemini GenerateContent and Vertex GenerateContent APIs; all other
|
|
420
|
+
* providers ignore it. Callers own the cache lifecycle and compatibility.
|
|
421
|
+
*/
|
|
422
|
+
cachedContent?: string;
|
|
396
423
|
/** Antigravity endpoint routing mode: "auto" (default with failover), "production", "sandbox". */
|
|
397
424
|
antigravityEndpointMode?: "auto" | "production" | "sandbox";
|
|
398
425
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuperGrok (`xai-oauth`) subscription usage provider.
|
|
3
|
+
*
|
|
4
|
+
* Reads utilization from the Grok CLI billing endpoint. Prefer the legacy
|
|
5
|
+
* weekly `format=credits` payload (creditUsagePercent / productUsage). When
|
|
6
|
+
* xAI marks the account as unified billing and omits those fields, fall back
|
|
7
|
+
* to the default monthly included-quota shape (`monthlyLimit` / `used`).
|
|
8
|
+
* Only OAuth access credentials are accepted; paid API keys are a separate
|
|
9
|
+
* product and must never be sent here.
|
|
10
|
+
*/
|
|
11
|
+
import type { UsageProvider } from "../usage.js";
|
|
12
|
+
export declare const xaiOauthUsageProvider: UsageProvider;
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface UsageWindow {
|
|
|
11
11
|
durationMs?: number;
|
|
12
12
|
/** Absolute reset timestamp in milliseconds since epoch. */
|
|
13
13
|
resetsAt?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Verb rendered before the {@link resetsAt} countdown (e.g. "tick", "regen").
|
|
16
|
+
* Defaults to "resets" — override for rolling windows where the timestamp is
|
|
17
|
+
* an incremental regeneration step rather than a full window reset.
|
|
18
|
+
*/
|
|
19
|
+
resetLabel?: string;
|
|
14
20
|
}
|
|
15
21
|
/** Quantitative usage data. */
|
|
16
22
|
export interface UsageAmount {
|
|
@@ -157,6 +163,7 @@ export declare const usageWindowSchema: import("arktype/internal/variants/object
|
|
|
157
163
|
label: string;
|
|
158
164
|
durationMs?: number | undefined;
|
|
159
165
|
resetsAt?: number | undefined;
|
|
166
|
+
resetLabel?: string | undefined;
|
|
160
167
|
}, {}>;
|
|
161
168
|
export declare const usageAmountSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
162
169
|
used?: number | undefined;
|
|
@@ -194,6 +201,7 @@ export declare const usageLimitSchema: import("arktype/internal/variants/object.
|
|
|
194
201
|
label: string;
|
|
195
202
|
durationMs?: number | undefined;
|
|
196
203
|
resetsAt?: number | undefined;
|
|
204
|
+
resetLabel?: string | undefined;
|
|
197
205
|
} | undefined;
|
|
198
206
|
amount: {
|
|
199
207
|
used?: number | undefined;
|
|
@@ -240,6 +248,7 @@ export declare const usageReportSchema: import("arktype/internal/variants/object
|
|
|
240
248
|
label: string;
|
|
241
249
|
durationMs?: number | undefined;
|
|
242
250
|
resetsAt?: number | undefined;
|
|
251
|
+
resetLabel?: string | undefined;
|
|
243
252
|
} | undefined;
|
|
244
253
|
amount: {
|
|
245
254
|
used?: number | undefined;
|
|
@@ -315,6 +324,8 @@ export interface UsageProvider {
|
|
|
315
324
|
supports?(params: UsageFetchParams): boolean;
|
|
316
325
|
/** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
|
|
317
326
|
validatesCredentials?: boolean;
|
|
327
|
+
/** Whether a failed refresh may serve the previous successful report. Defaults to true. */
|
|
328
|
+
retainLastGoodOnFailure?: boolean;
|
|
318
329
|
}
|
|
319
330
|
/** Request context used when ranking usage for a specific model. */
|
|
320
331
|
export interface CredentialRankingContext {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.0
|
|
4
|
+
"version": "17.1.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.0
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.0
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.0
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.1.0",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.1.0",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.1.0",
|
|
44
44
|
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
@@ -18,7 +18,7 @@ import { YAML } from "bun";
|
|
|
18
18
|
import { AuthStorage } from "../auth-storage";
|
|
19
19
|
import * as AIError from "../error";
|
|
20
20
|
import { AuthBrokerClient } from "./client";
|
|
21
|
-
import { RemoteAuthCredentialStore } from "./remote-store";
|
|
21
|
+
import { type AuthBrokerAccountPool, RemoteAuthCredentialStore } from "./remote-store";
|
|
22
22
|
import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "./snapshot-cache";
|
|
23
23
|
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
|
|
24
24
|
|
|
@@ -37,6 +37,8 @@ export interface DiscoverAuthStorageOptions {
|
|
|
37
37
|
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
38
38
|
cachePath?: string;
|
|
39
39
|
sourceLabel?: string;
|
|
40
|
+
/** Programmatic pool for SDK hosts. Takes precedence over the environment file. */
|
|
41
|
+
accountPool?: AuthBrokerAccountPool;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Path to the local bearer token file. Created by `omp auth-broker token`. */
|
|
@@ -112,6 +114,57 @@ async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
|
112
114
|
return {};
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
export async function loadAuthBrokerAccountPool(): Promise<AuthBrokerAccountPool | undefined> {
|
|
118
|
+
const filePath = process.env.OMP_AUTH_BROKER_ACCOUNT_POOL_FILE?.trim();
|
|
119
|
+
if (!filePath) return undefined;
|
|
120
|
+
|
|
121
|
+
let parsed: unknown;
|
|
122
|
+
try {
|
|
123
|
+
parsed = await Bun.file(filePath).json();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new AIError.ConfigurationError(`Unable to read OMP_AUTH_BROKER_ACCOUNT_POOL_FILE at ${filePath}`, {
|
|
126
|
+
cause: error,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
130
|
+
throw new AIError.ConfigurationError("OMP_AUTH_BROKER_ACCOUNT_POOL_FILE must contain a JSON object");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const accountPool = new Map<string, ReadonlySet<string>>();
|
|
134
|
+
for (const [provider, value] of Object.entries(parsed)) {
|
|
135
|
+
const normalizedProvider = provider.trim();
|
|
136
|
+
if (normalizedProvider.length === 0) {
|
|
137
|
+
throw new AIError.ConfigurationError("OMP_AUTH_BROKER_ACCOUNT_POOL_FILE contains an empty provider id");
|
|
138
|
+
}
|
|
139
|
+
if (provider !== normalizedProvider) {
|
|
140
|
+
throw new AIError.ConfigurationError(
|
|
141
|
+
"OMP_AUTH_BROKER_ACCOUNT_POOL_FILE contains a provider id with surrounding whitespace",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(value)) {
|
|
145
|
+
throw new AIError.ConfigurationError(
|
|
146
|
+
`OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} must be an array of identity keys`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const identities = new Set<string>();
|
|
150
|
+
for (const identity of value) {
|
|
151
|
+
if (typeof identity !== "string" || identity.length === 0) {
|
|
152
|
+
throw new AIError.ConfigurationError(
|
|
153
|
+
`OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an invalid identity key`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (identity !== identity.trim()) {
|
|
157
|
+
throw new AIError.ConfigurationError(
|
|
158
|
+
`OMP_AUTH_BROKER_ACCOUNT_POOL_FILE entry for ${provider} contains an identity key with surrounding whitespace`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
identities.add(identity);
|
|
162
|
+
}
|
|
163
|
+
accountPool.set(provider, identities);
|
|
164
|
+
}
|
|
165
|
+
return accountPool;
|
|
166
|
+
}
|
|
167
|
+
|
|
115
168
|
function resolveSnapshotTtlMs(): number {
|
|
116
169
|
const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS;
|
|
117
170
|
if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
@@ -183,6 +236,7 @@ export async function discoverAuthStorage(options: DiscoverAuthStorageOptions =
|
|
|
183
236
|
});
|
|
184
237
|
|
|
185
238
|
if (brokerConfig) {
|
|
239
|
+
const accountPool = options.accountPool ?? (await loadAuthBrokerAccountPool());
|
|
186
240
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
187
241
|
const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath();
|
|
188
242
|
const ttlMs = resolveSnapshotTtlMs();
|
|
@@ -226,6 +280,7 @@ export async function discoverAuthStorage(options: DiscoverAuthStorageOptions =
|
|
|
226
280
|
client,
|
|
227
281
|
initialSnapshot,
|
|
228
282
|
onSnapshot: persist,
|
|
283
|
+
accountPool,
|
|
229
284
|
});
|
|
230
285
|
const storage = new AuthStorage(store, {
|
|
231
286
|
configValueResolver: options.configValueResolver,
|
|
@@ -31,6 +31,23 @@ import type {
|
|
|
31
31
|
SnapshotStreamEvent,
|
|
32
32
|
} from "./types";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Per-provider OAuth identities visible to this trusted broker client.
|
|
36
|
+
* Missing providers are unrestricted; an empty set excludes that provider's
|
|
37
|
+
* OAuth credentials. API keys are never filtered.
|
|
38
|
+
*/
|
|
39
|
+
export type AuthBrokerAccountPool = ReadonlyMap<string, ReadonlySet<string>>;
|
|
40
|
+
|
|
41
|
+
function isCredentialInAccountPool(
|
|
42
|
+
entry: Pick<SnapshotEntry, "provider" | "credential" | "identityKey">,
|
|
43
|
+
accountPool: AuthBrokerAccountPool | undefined,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (entry.credential.type !== "oauth") return true;
|
|
46
|
+
const identities = accountPool?.get(entry.provider);
|
|
47
|
+
if (identities === undefined) return true;
|
|
48
|
+
return entry.identityKey !== null && identities.has(entry.identityKey);
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
/**
|
|
35
52
|
* Client-side TTL for the aggregate `/v1/usage` response. The broker dedups
|
|
36
53
|
* upstream `/usage` hits via AuthStorage's 5-minute per-credential cache plus
|
|
@@ -205,16 +222,23 @@ export interface RemoteAuthCredentialStoreOptions {
|
|
|
205
222
|
*/
|
|
206
223
|
streamSnapshots?: boolean;
|
|
207
224
|
/**
|
|
208
|
-
* Called
|
|
209
|
-
*
|
|
225
|
+
* Called with each broker-sourced raw full snapshot after the filtered
|
|
226
|
+
* public view is applied. The constructor's initial snapshot intentionally
|
|
227
|
+
* does not trigger this hook.
|
|
210
228
|
*/
|
|
211
229
|
onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
|
|
230
|
+
/**
|
|
231
|
+
* OAuth identities visible through this store. This is a trusted-client
|
|
232
|
+
* routing policy, not broker authorization.
|
|
233
|
+
*/
|
|
234
|
+
accountPool?: AuthBrokerAccountPool;
|
|
212
235
|
}
|
|
213
236
|
|
|
214
237
|
export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
215
238
|
readonly #client: AuthBrokerClient;
|
|
216
239
|
readonly #streamSnapshots: boolean;
|
|
217
240
|
readonly #onSnapshot?: (snapshot: SnapshotResponse, generation: number) => void;
|
|
241
|
+
readonly #accountPool?: AuthBrokerAccountPool;
|
|
218
242
|
#snapshot: SnapshotResponse = emptySnapshot();
|
|
219
243
|
#snapshotReceivedAt = Date.now();
|
|
220
244
|
#generation = 0;
|
|
@@ -239,6 +263,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
239
263
|
constructor(opts: RemoteAuthCredentialStoreOptions) {
|
|
240
264
|
this.#client = opts.client;
|
|
241
265
|
this.#streamSnapshots = opts.streamSnapshots ?? true;
|
|
266
|
+
this.#accountPool = opts.accountPool
|
|
267
|
+
? new Map([...opts.accountPool].map(([provider, identities]) => [provider, new Set(identities)]))
|
|
268
|
+
: undefined;
|
|
242
269
|
this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
|
|
243
270
|
this.#onSnapshot = opts.onSnapshot;
|
|
244
271
|
void this.#runBackground();
|
|
@@ -255,7 +282,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
255
282
|
#applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
|
|
256
283
|
const nowMs = Date.now();
|
|
257
284
|
const previousCredentials = this.#snapshot.credentials;
|
|
258
|
-
const credentials = snapshot.credentials
|
|
285
|
+
const credentials = snapshot.credentials
|
|
286
|
+
.filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
|
|
287
|
+
.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
|
|
259
288
|
if (snapshotBlocksChanged(previousCredentials, credentials)) this.#invalidateUsageCache();
|
|
260
289
|
if (protectNewBlocks) this.#protectNewSnapshotBlocks(previousCredentials, credentials, nowMs);
|
|
261
290
|
this.#snapshot = { ...snapshot, credentials };
|
|
@@ -264,7 +293,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
264
293
|
const onSnapshot = this.#onSnapshot;
|
|
265
294
|
if (!onSnapshot) return;
|
|
266
295
|
try {
|
|
267
|
-
onSnapshot(
|
|
296
|
+
onSnapshot(snapshot, generation);
|
|
268
297
|
} catch (error) {
|
|
269
298
|
logger.debug("auth-broker snapshot callback failed", { error: String(error) });
|
|
270
299
|
}
|
|
@@ -383,6 +412,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
383
412
|
generation: number,
|
|
384
413
|
serverNowMs: number,
|
|
385
414
|
): void {
|
|
415
|
+
if (!isCredentialInAccountPool(entry, this.#accountPool)) {
|
|
416
|
+
this.#removeStreamCredential(entry.id, refresher, generation, serverNowMs);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
386
419
|
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
|
|
387
420
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
|
|
388
421
|
const previousBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
@@ -577,7 +610,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
577
610
|
if (entry.credential.type !== "oauth") {
|
|
578
611
|
throw new AIError.AuthBrokerError(`Broker returned non-OAuth credential for id=${credentialId}`);
|
|
579
612
|
}
|
|
580
|
-
this.#applyCredentialEntry(entry)
|
|
613
|
+
if (!this.#applyCredentialEntry(entry)) {
|
|
614
|
+
throw new AIError.AuthBrokerError(
|
|
615
|
+
`Broker refreshed credential id=${credentialId} outside the configured account pool`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
581
618
|
this.#maybeRefreshSnapshot("suspect credential refresh");
|
|
582
619
|
}
|
|
583
620
|
|
|
@@ -677,20 +714,27 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
677
714
|
.map(entry => [entry.id, entry.blocks] as const),
|
|
678
715
|
);
|
|
679
716
|
const others = this.#snapshot.credentials.filter(entry => entry.provider !== provider);
|
|
680
|
-
const incoming = entries
|
|
717
|
+
const incoming = entries
|
|
718
|
+
.filter(entry => isCredentialInAccountPool(entry, this.#accountPool))
|
|
719
|
+
.map(entry => credentialEntryWithBlocks(entry, existingBlocks.get(entry.id)));
|
|
681
720
|
this.#snapshot = { ...this.#snapshot, credentials: [...others, ...incoming] };
|
|
682
721
|
}
|
|
683
|
-
#applyCredentialEntry(entry: AuthCredentialSnapshotEntry):
|
|
722
|
+
#applyCredentialEntry(entry: AuthCredentialSnapshotEntry): boolean {
|
|
723
|
+
if (!isCredentialInAccountPool(entry, this.#accountPool)) {
|
|
724
|
+
this.#removeCredentialById(entry.id);
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
684
727
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === entry.id);
|
|
685
728
|
const existingBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
686
729
|
const incoming = credentialEntryWithBlocks(entry, existingBlocks);
|
|
687
730
|
if (index === -1) {
|
|
688
731
|
this.#snapshot = { ...this.#snapshot, credentials: [...this.#snapshot.credentials, incoming] };
|
|
689
|
-
return;
|
|
732
|
+
return true;
|
|
690
733
|
}
|
|
691
734
|
const credentials = [...this.#snapshot.credentials];
|
|
692
735
|
credentials[index] = incoming;
|
|
693
736
|
this.#snapshot = { ...this.#snapshot, credentials };
|
|
737
|
+
return true;
|
|
694
738
|
}
|
|
695
739
|
|
|
696
740
|
#removeProviderEntries(provider: string): void {
|
|
@@ -837,14 +881,19 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
837
881
|
signal?: AbortSignal,
|
|
838
882
|
): Promise<OAuthCredentials> {
|
|
839
883
|
const { entry } = await this.#client.refreshCredential(credentialId, signal);
|
|
884
|
+
if (entry.credential.type !== "oauth") {
|
|
885
|
+
throw new AIError.AuthBrokerError(`Broker returned non-OAuth credential for id=${credentialId}`);
|
|
886
|
+
}
|
|
887
|
+
if (!this.#applyCredentialEntry(entry)) {
|
|
888
|
+
throw new AIError.AuthBrokerError(
|
|
889
|
+
`Broker refreshed credential id=${credentialId} outside the configured account pool`,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
840
892
|
if (!this.#streamingActive) {
|
|
841
893
|
await this.refreshSnapshot().catch(error => {
|
|
842
894
|
logger.debug("auth-broker snapshot refresh after credential refresh failed", { error: String(error) });
|
|
843
895
|
});
|
|
844
896
|
}
|
|
845
|
-
if (entry.credential.type !== "oauth") {
|
|
846
|
-
throw new AIError.AuthBrokerError(`Broker returned non-OAuth credential for id=${credentialId}`);
|
|
847
|
-
}
|
|
848
897
|
const refreshed = entry.credential;
|
|
849
898
|
return {
|
|
850
899
|
access: refreshed.access,
|
|
@@ -865,7 +914,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
865
914
|
*/
|
|
866
915
|
async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
|
|
867
916
|
const reports = await this.#raceWithSignal(this.#loadUsageReports(), signal);
|
|
868
|
-
|
|
917
|
+
if (!reports) return null;
|
|
918
|
+
return this.#filterUsageReports(this.#applyUsageOverlays(reports));
|
|
869
919
|
}
|
|
870
920
|
|
|
871
921
|
/**
|
|
@@ -883,12 +933,27 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
883
933
|
signal?: AbortSignal,
|
|
884
934
|
): Promise<UsageReport | null> {
|
|
885
935
|
const reports = await this.#raceWithSignal(this.#loadUsageReports(), signal);
|
|
886
|
-
const
|
|
936
|
+
const visibleReports = reports ? this.#filterUsageReports(reports) : null;
|
|
937
|
+
const matched = visibleReports ? matchUsageReport(visibleReports, provider, credential) : null;
|
|
887
938
|
const overlay = this.#getActiveUsageOverlay(provider, credential);
|
|
888
939
|
if (matched && overlay) return mergeUsageReports(matched, overlay);
|
|
889
940
|
return overlay ?? matched;
|
|
890
941
|
}
|
|
891
942
|
|
|
943
|
+
#filterUsageReports(reports: UsageReport[]): UsageReport[] {
|
|
944
|
+
const accountPool = this.#accountPool;
|
|
945
|
+
if (!accountPool) return reports;
|
|
946
|
+
return reports.filter(report => {
|
|
947
|
+
if (!accountPool.has(report.provider)) return true;
|
|
948
|
+
return this.#snapshot.credentials.some(
|
|
949
|
+
entry =>
|
|
950
|
+
entry.provider === report.provider &&
|
|
951
|
+
entry.credential.type === "oauth" &&
|
|
952
|
+
usageReportMatchesCredential(report, entry.credential),
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
892
957
|
ingestUsageReport(provider: Provider, credential: OAuthCredential, report: UsageReport): boolean {
|
|
893
958
|
const key = usageOverlayKey(provider, credential);
|
|
894
959
|
if (!key) return false;
|
|
@@ -1059,6 +1124,21 @@ function matchUsageReport(reports: UsageReport[], provider: Provider, credential
|
|
|
1059
1124
|
return null;
|
|
1060
1125
|
}
|
|
1061
1126
|
|
|
1127
|
+
function usageReportMatchesCredential(report: UsageReport, credential: OAuthCredential): boolean {
|
|
1128
|
+
const metadata = (report.metadata ?? {}) as Record<string, unknown>;
|
|
1129
|
+
const credentialOrg = credential.orgId?.trim().toLowerCase();
|
|
1130
|
+
const reportOrg = readMetadataString(metadata, "orgId")?.toLowerCase();
|
|
1131
|
+
if (credentialOrg !== reportOrg) return false;
|
|
1132
|
+
|
|
1133
|
+
const accountId = credential.accountId?.trim().toLowerCase();
|
|
1134
|
+
const email = credential.email?.trim().toLowerCase();
|
|
1135
|
+
const projectId = credential.projectId?.trim().toLowerCase();
|
|
1136
|
+
if (accountId || email || projectId) {
|
|
1137
|
+
return reportMatchesIdentity(report, accountId, email, projectId);
|
|
1138
|
+
}
|
|
1139
|
+
return credentialOrg !== undefined;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1062
1142
|
function findMatchingReportIndex(reports: UsageReport[], overlay: UsageReport): number {
|
|
1063
1143
|
const all = reports
|
|
1064
1144
|
.map((report, index) => ({ report, index }))
|