@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/types/auth-broker/discover.d.ts +4 -0
  4. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  5. package/dist/types/auth-storage.d.ts +45 -0
  6. package/dist/types/index.d.ts +2 -0
  7. package/dist/types/providers/google-shared.d.ts +11 -0
  8. package/dist/types/providers/openai-chat-wire.d.ts +25 -0
  9. package/dist/types/providers/openai-completions.d.ts +3 -0
  10. package/dist/types/providers/openai-responses-wire.d.ts +17 -0
  11. package/dist/types/providers/openai-responses.d.ts +8 -2
  12. package/dist/types/providers/openai-shared.d.ts +32 -11
  13. package/dist/types/providers/transform-messages.d.ts +7 -0
  14. package/dist/types/registry/alibaba-token-plan.d.ts +7 -0
  15. package/dist/types/registry/meta.d.ts +7 -0
  16. package/dist/types/registry/oauth/google-gemini-cli.d.ts +12 -0
  17. package/dist/types/registry/oauth/google-oauth-shared.d.ts +35 -1
  18. package/dist/types/registry/oauth/xai-oauth.d.ts +25 -9
  19. package/dist/types/registry/oauth/zai.d.ts +25 -0
  20. package/dist/types/registry/registry.d.ts +15 -0
  21. package/dist/types/registry/zai.d.ts +8 -0
  22. package/dist/types/types.d.ts +27 -0
  23. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  24. package/dist/types/usage/synthetic.d.ts +2 -0
  25. package/dist/types/usage/xai-oauth.d.ts +12 -0
  26. package/dist/types/usage.d.ts +11 -0
  27. package/package.json +4 -4
  28. package/src/auth-broker/discover.ts +56 -1
  29. package/src/auth-broker/remote-store.ts +93 -13
  30. package/src/auth-storage.ts +258 -15
  31. package/src/index.ts +2 -0
  32. package/src/providers/__tests__/kimi-code-thinking.test.ts +44 -0
  33. package/src/providers/amazon-bedrock.ts +54 -37
  34. package/src/providers/azure-openai-responses.ts +6 -0
  35. package/src/providers/gitlab-duo.ts +4 -0
  36. package/src/providers/google-shared.ts +30 -0
  37. package/src/providers/openai-anthropic-shim.ts +2 -0
  38. package/src/providers/openai-chat-server.ts +30 -0
  39. package/src/providers/openai-chat-wire.ts +12 -0
  40. package/src/providers/openai-completions.ts +70 -2
  41. package/src/providers/openai-responses-server.ts +22 -0
  42. package/src/providers/openai-responses-wire.ts +8 -0
  43. package/src/providers/openai-responses.ts +227 -8
  44. package/src/providers/openai-shared.ts +121 -11
  45. package/src/providers/pi-native-client.ts +9 -0
  46. package/src/providers/pi-native-server.ts +3 -0
  47. package/src/providers/transform-messages.ts +64 -23
  48. package/src/registry/alibaba-token-plan.ts +43 -0
  49. package/src/registry/meta.ts +22 -0
  50. package/src/registry/oauth/__tests__/xai-oauth.test.ts +214 -5
  51. package/src/registry/oauth/callback-server.ts +19 -12
  52. package/src/registry/oauth/google-antigravity.ts +34 -18
  53. package/src/registry/oauth/google-gemini-cli.ts +56 -32
  54. package/src/registry/oauth/google-oauth-shared.ts +107 -21
  55. package/src/registry/oauth/xai-oauth.ts +146 -17
  56. package/src/registry/oauth/zai.ts +285 -0
  57. package/src/registry/registry.ts +6 -1
  58. package/src/registry/zai.ts +14 -0
  59. package/src/stream.ts +53 -1
  60. package/src/types.ts +28 -0
  61. package/src/usage/alibaba-token-plan.ts +202 -0
  62. package/src/usage/claude.ts +125 -1
  63. package/src/usage/synthetic.ts +180 -0
  64. package/src/usage/xai-oauth.ts +414 -0
  65. package/src/usage/zai.ts +9 -3
  66. 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
+ };
@@ -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,3 @@
1
+ import type { CredentialRankingStrategy, UsageProvider } from "../usage.js";
2
+ export declare const alibabaTokenPlanUsageProvider: UsageProvider;
3
+ export declare const alibabaTokenPlanRankingStrategy: CredentialRankingStrategy;
@@ -0,0 +1,2 @@
1
+ import type { UsageProvider } from "../usage.js";
2
+ export declare const syntheticUsageProvider: UsageProvider;
@@ -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;
@@ -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.8",
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.8",
42
- "@oh-my-pi/pi-utils": "17.0.8",
43
- "@oh-my-pi/pi-wire": "17.0.8",
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 after broker-sourced full snapshots are applied. The constructor's
209
- * initial snapshot intentionally does not trigger this hook.
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.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
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(this.#snapshot, generation);
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.map(entry => credentialEntryWithBlocks(entry, existingBlocks.get(entry.id)));
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): void {
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
- return reports ? this.#applyUsageOverlays(reports) : null;
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 matched = reports ? matchUsageReport(reports, provider, credential) : null;
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 }))