@oh-my-pi/pi-ai 16.3.14 → 16.4.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 (53) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +5 -2
  3. package/dist/types/auth-broker/remote-store.d.ts +1 -0
  4. package/dist/types/auth-broker/wire-schemas.d.ts +8 -0
  5. package/dist/types/auth-storage.d.ts +5 -0
  6. package/dist/types/providers/azure-openai-responses.d.ts +1 -1
  7. package/dist/types/providers/ollama.d.ts +1 -1
  8. package/dist/types/providers/openai-chat-server-schema.d.ts +1 -1
  9. package/dist/types/providers/openai-chat-wire.d.ts +1 -1
  10. package/dist/types/providers/openai-codex/request-transformer.d.ts +43 -5
  11. package/dist/types/providers/openai-codex-responses.d.ts +51 -8
  12. package/dist/types/providers/openai-completions.d.ts +1 -1
  13. package/dist/types/providers/openai-responses-wire.d.ts +16 -7
  14. package/dist/types/providers/openai-responses.d.ts +1 -1
  15. package/dist/types/providers/openai-shared.d.ts +38 -8
  16. package/dist/types/registry/novita.d.ts +6 -0
  17. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  18. package/dist/types/registry/oauth/index.d.ts +1 -25
  19. package/dist/types/registry/oauth/xai-oauth.d.ts +9 -25
  20. package/dist/types/registry/registry.d.ts +4 -1
  21. package/dist/types/registry/xai-oauth.d.ts +0 -1
  22. package/dist/types/types.d.ts +26 -3
  23. package/package.json +4 -4
  24. package/src/auth-broker/remote-store.ts +60 -5
  25. package/src/auth-broker/server.ts +1 -0
  26. package/src/auth-broker/wire-schemas.ts +1 -0
  27. package/src/auth-gateway/server.ts +2 -2
  28. package/src/auth-storage.ts +198 -60
  29. package/src/error/flags.ts +4 -1
  30. package/src/error/rate-limit.ts +7 -1
  31. package/src/providers/amazon-bedrock.ts +1 -0
  32. package/src/providers/anthropic-messages-server.ts +18 -0
  33. package/src/providers/azure-openai-responses.ts +3 -3
  34. package/src/providers/ollama.ts +1 -1
  35. package/src/providers/openai-chat-server-schema.ts +1 -1
  36. package/src/providers/openai-chat-server.ts +8 -1
  37. package/src/providers/openai-chat-wire.ts +1 -1
  38. package/src/providers/openai-codex/request-transformer.ts +94 -41
  39. package/src/providers/openai-codex-responses.ts +475 -39
  40. package/src/providers/openai-completions.ts +38 -12
  41. package/src/providers/openai-responses-server.ts +8 -1
  42. package/src/providers/openai-responses-wire.ts +16 -7
  43. package/src/providers/openai-responses.ts +18 -5
  44. package/src/providers/openai-shared.ts +125 -13
  45. package/src/registry/novita.ts +22 -0
  46. package/src/registry/oauth/__tests__/xai-oauth.test.ts +191 -80
  47. package/src/registry/oauth/device-code.ts +92 -0
  48. package/src/registry/oauth/index.ts +1 -91
  49. package/src/registry/oauth/xai-oauth.ts +231 -191
  50. package/src/registry/registry.ts +2 -0
  51. package/src/registry/xai-oauth.ts +0 -1
  52. package/src/stream.ts +7 -1
  53. package/src/types.ts +29 -3
@@ -1,14 +1,11 @@
1
1
  import type { FetchImpl } from "../../types.js";
2
- import { OAuthCallbackFlow } from "./callback-server.js";
3
2
  import type { OAuthController, OAuthCredentials } from "./types.js";
4
3
  /**
5
- * Validate an xAI OIDC discovery endpoint against scheme + host.
4
+ * Validate an xAI OIDC endpoint against its scheme and host.
6
5
  *
7
- * Hermes `_xai_validate_oauth_endpoint` L2997-3035. The discovery response is
8
- * long-lived and cached in {@link OAuthCredentials}; a single MITM during
9
- * initial login could substitute a malicious `token_endpoint` that would then
10
- * receive every future refresh_token. Rejecting non-HTTPS or non-`x.ai` /
11
- * `*.x.ai` hosts pins the cached endpoint to the xAI auth origin.
6
+ * The discovery response is long-lived and its token endpoint receives every
7
+ * future refresh token. Rejecting non-HTTPS or non-`x.ai` / `*.x.ai` hosts
8
+ * pins that endpoint to the xAI auth origin.
12
9
  *
13
10
  * @throws Error with message `Invalid xAI <field>: <url>` when the URL fails
14
11
  * either scheme or host validation.
@@ -18,29 +15,16 @@ export declare function validateXAIEndpoint(url: string, field: string): string;
18
15
  * Check whether a JWT access token is at or past its `exp` claim (with an
19
16
  * optional refresh-skew margin).
20
17
  *
21
- * Hermes `_xai_access_token_is_expiring` L2979-2994. Returns `false` for any
22
- * malformed input — this is a refresh-trigger check, not a validation, so
23
- * non-JWTs ("no token in cache") must NOT trigger a spurious refresh.
18
+ * Returns `false` for malformed input because this is a refresh-trigger check,
19
+ * not token validation.
24
20
  */
25
21
  export declare function isXAIAccessTokenExpiring(jwt: string, skewSeconds?: number): boolean;
26
- /**
27
- * xAI Grok OAuth code flow (Hermes `_xai_oauth_loopback_login` L5315-5469).
28
- */
29
- export declare class XAIOAuthFlow extends OAuthCallbackFlow {
30
- #private;
31
- constructor(ctrl: OAuthController);
32
- generateAuthUrl(state: string, redirectUri: string): Promise<{
33
- url: string;
34
- instructions?: string;
35
- }>;
36
- exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
37
- }
22
+ /** Log in to xAI Grok with the RFC 8628 device authorization grant. */
38
23
  export declare function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredentials>;
39
24
  /**
40
25
  * Refresh an xAI OAuth access token using a stored refresh_token.
41
26
  *
42
- * Hermes `refresh_xai_oauth_pure` L3087-3160. Re-runs OIDC discovery and
43
- * re-validates the cached `token_endpoint` on the refresh hot path so a
44
- * cached-but-poisoned endpoint cannot silently leak a refresh_token.
27
+ * Re-runs OIDC discovery and re-validates the token endpoint before sending
28
+ * the stored refresh token.
45
29
  */
46
30
  export declare function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials>;
@@ -165,6 +165,10 @@ declare const ALL: ({
165
165
  readonly id: "nanogpt";
166
166
  readonly name: "NanoGPT";
167
167
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
168
+ } | {
169
+ id: "novita";
170
+ name: string;
171
+ login: (options: import("./oauth/index.js").OAuthController) => Promise<string>;
168
172
  } | {
169
173
  readonly id: "nvidia";
170
174
  readonly name: "NVIDIA";
@@ -268,7 +272,6 @@ declare const ALL: ({
268
272
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
269
273
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<import("./oauth/index.js").OAuthCredentials>;
270
274
  readonly refreshToken: (credentials: import("./oauth/index.js").OAuthCredentials) => Promise<import("./oauth/index.js").OAuthCredentials>;
271
- readonly pasteCodeFlow: true;
272
275
  } | {
273
276
  readonly id: "xiaomi";
274
277
  readonly name: "Xiaomi MiMo";
@@ -4,5 +4,4 @@ export declare const xaiOauthProvider: {
4
4
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
5
5
  readonly login: (cb: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
6
6
  readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
7
- readonly pasteCodeFlow: true;
8
7
  };
@@ -166,6 +166,27 @@ export interface RawSseEvent {
166
166
  data: string;
167
167
  raw: string[];
168
168
  }
169
+ /** Lifecycle fields shared by every Codex compaction implementation. */
170
+ export interface CodexCompactionContext {
171
+ /** Stable only for one logical compaction, including parallel summary calls. */
172
+ operationId: string;
173
+ trigger: "manual" | "auto";
174
+ reason: "user_requested" | "context_limit" | "model_downshift" | "comp_hash_changed";
175
+ phase: "standalone_turn" | "pre_turn" | "mid_turn";
176
+ strategy: "memento" | "prefix_compaction";
177
+ }
178
+ /** Canonical nested metadata serialized into the Codex turn envelope. */
179
+ export interface CodexCompactionMetadata {
180
+ trigger: "manual" | "auto";
181
+ reason: "user_requested" | "context_limit" | "model_downshift" | "comp_hash_changed";
182
+ implementation: "responses" | "responses_compaction_v2" | "responses_compact";
183
+ phase: "standalone_turn" | "pre_turn" | "mid_turn";
184
+ strategy: "memento" | "prefix_compaction";
185
+ }
186
+ /** Dispatch context combining canonical metadata with its local operation identity. */
187
+ export interface CodexCompactionRequestContext extends CodexCompactionMetadata {
188
+ operationId: string;
189
+ }
169
190
  export interface StreamOptions {
170
191
  temperature?: number;
171
192
  topP?: number;
@@ -231,9 +252,9 @@ export interface StreamOptions {
231
252
  */
232
253
  sessionId?: string;
233
254
  /**
234
- * Optional prompt-cache identity. When set, OpenAI Responses-compatible
235
- * providers use this for `prompt_cache_key` while keeping `sessionId` for
236
- * provider routing / conversation headers.
255
+ * Optional prompt-cache identity. OpenAI-family providers use this for
256
+ * `prompt_cache_key` payloads and cache-affinity headers such as
257
+ * `x-grok-conv-id`; when omitted, they fall back to `sessionId`.
237
258
  */
238
259
  promptCacheKey?: string;
239
260
  /**
@@ -241,6 +262,8 @@ export interface StreamOptions {
241
262
  * Providers can use this to persist transport/session state between turns.
242
263
  */
243
264
  providerSessionState?: Map<string, ProviderSessionState>;
265
+ /** Canonical Codex compaction classification; ignored by other providers. */
266
+ codexCompaction?: CodexCompactionRequestContext;
244
267
  /**
245
268
  * Force Gemini model-mode Interactions API transport for providers that support it.
246
269
  * When unset, those providers may still use Interactions to continue known
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.3.14",
4
+ "version": "16.4.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.0",
41
- "@oh-my-pi/pi-catalog": "16.3.14",
42
- "@oh-my-pi/pi-utils": "16.3.14",
43
- "@oh-my-pi/pi-wire": "16.3.14",
41
+ "@oh-my-pi/pi-catalog": "16.4.0",
42
+ "@oh-my-pi/pi-utils": "16.4.0",
43
+ "@oh-my-pi/pi-wire": "16.4.0",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
@@ -39,6 +39,7 @@ import type {
39
39
  * one broker call instead of N.
40
40
  */
41
41
  const USAGE_CACHE_TTL_MS = 15_000;
42
+ const CREDENTIAL_BLOCK_RECONCILE_DELAY_MS = 5 * 60_000;
42
43
  const WAIT_THRESHOLD_MS = 1_000;
43
44
  const MAX_WAIT_MS = 5_000;
44
45
  const BACKGROUND_WAIT_MS = 30_000;
@@ -50,7 +51,9 @@ function compareCredentialBlockSnapshots(a: CredentialBlockSnapshot, b: Credenti
50
51
  if (provider !== 0) return provider;
51
52
  const scope = a.blockScope.localeCompare(b.blockScope);
52
53
  if (scope !== 0) return scope;
53
- return a.blockedUntilMs - b.blockedUntilMs;
54
+ const blockedUntil = a.blockedUntilMs - b.blockedUntilMs;
55
+ if (blockedUntil !== 0) return blockedUntil;
56
+ return (a.updatedAtMs ?? 0) - (b.updatedAtMs ?? 0);
54
57
  }
55
58
 
56
59
  function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBlockSnapshot {
@@ -58,6 +61,7 @@ function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBloc
58
61
  providerKey: block.providerKey,
59
62
  blockScope: block.blockScope,
60
63
  blockedUntilMs: block.blockedUntilMs,
64
+ ...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
61
65
  };
62
66
  }
63
67
 
@@ -74,7 +78,8 @@ function credentialBlockSnapshotsEqual(
74
78
  if (
75
79
  leftBlock.providerKey !== rightBlock.providerKey ||
76
80
  leftBlock.blockScope !== rightBlock.blockScope ||
77
- leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs
81
+ leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs ||
82
+ leftBlock.updatedAtMs !== rightBlock.updatedAtMs
78
83
  ) {
79
84
  return false;
80
85
  }
@@ -208,6 +213,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
208
213
  #cache: Map<string, CacheEntry> = new Map();
209
214
  #usageCache?: UsageCacheEntry;
210
215
  #usageInflight?: Promise<UsageReport[] | null>;
216
+ #credentialBlockReconcileAfter: Map<string, number> = new Map();
211
217
  #usageCacheEpoch = 0;
212
218
  #closed = false;
213
219
  /**
@@ -236,10 +242,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
236
242
  return this.#snapshot;
237
243
  }
238
244
 
239
- #applySnapshot(snapshot: SnapshotResponse, generation: number): void {
245
+ #applySnapshot(snapshot: SnapshotResponse, generation: number, protectNewBlocks = true): void {
240
246
  const nowMs = Date.now();
247
+ const previousCredentials = this.#snapshot.credentials;
241
248
  const credentials = snapshot.credentials.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
242
- if (snapshotBlocksChanged(this.#snapshot.credentials, credentials)) this.#invalidateUsageCache();
249
+ if (snapshotBlocksChanged(previousCredentials, credentials)) this.#invalidateUsageCache();
250
+ if (protectNewBlocks) this.#protectNewSnapshotBlocks(previousCredentials, credentials, nowMs);
243
251
  this.#snapshot = { ...snapshot, credentials };
244
252
  this.#generation = generation;
245
253
  this.#snapshotReceivedAt = nowMs;
@@ -251,6 +259,34 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
251
259
  logger.debug("auth-broker snapshot callback failed", { error: String(error) });
252
260
  }
253
261
  }
262
+ #protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
263
+ const previousBlocksByKey = new Map<string, string>();
264
+ for (const entry of previous) {
265
+ for (const block of entry.blocks ?? []) {
266
+ previousBlocksByKey.set(
267
+ `${entry.id}\0${block.providerKey}\0${block.blockScope}`,
268
+ `${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`,
269
+ );
270
+ }
271
+ }
272
+ const activeKeys = new Set<string>();
273
+ for (const entry of next) {
274
+ for (const block of entry.blocks ?? []) {
275
+ const key = `${entry.id}\0${block.providerKey}\0${block.blockScope}`;
276
+ activeKeys.add(key);
277
+ const signature = `${block.blockedUntilMs}\0${block.updatedAtMs ?? ""}`;
278
+ if (previousBlocksByKey.get(key) === signature) continue;
279
+ const updatedAtMs = block.updatedAtMs ?? nowMs;
280
+ this.#credentialBlockReconcileAfter.set(
281
+ key,
282
+ Math.min(block.blockedUntilMs, updatedAtMs + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
283
+ );
284
+ }
285
+ }
286
+ for (const key of this.#credentialBlockReconcileAfter.keys()) {
287
+ if (!activeKeys.has(key)) this.#credentialBlockReconcileAfter.delete(key);
288
+ }
289
+ }
254
290
 
255
291
  async #runBackground(): Promise<void> {
256
292
  let backoffMs = BACKGROUND_BACKOFF_INITIAL_MS;
@@ -340,11 +376,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
340
376
  const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
341
377
  const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
342
378
  const previousBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
343
- if (!credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks)) this.#invalidateUsageCache();
379
+ const blocksChanged = !credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks);
380
+ if (blocksChanged) this.#invalidateUsageCache();
344
381
  const credentials =
345
382
  index === -1
346
383
  ? [...this.#snapshot.credentials, incoming]
347
384
  : this.#snapshot.credentials.map((candidate, i) => (i === index ? incoming : candidate));
385
+ if (blocksChanged) this.#protectNewSnapshotBlocks(this.#snapshot.credentials, credentials, Date.now());
348
386
  this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
349
387
  this.#generation = generation;
350
388
  this.#snapshotReceivedAt = Date.now();
@@ -392,6 +430,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
392
430
  return block.blockedUntilMs;
393
431
  }
394
432
 
433
+ getCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number | undefined {
434
+ if (this.getCredentialBlock(credentialId, providerKey, blockScope) === undefined) return undefined;
435
+ return this.#credentialBlockReconcileAfter.get(`${credentialId}\0${providerKey}\0${blockScope}`);
436
+ }
437
+
395
438
  listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
396
439
  const nowMs = Date.now();
397
440
  this.cleanExpiredCredentialBlocks(nowMs);
@@ -406,6 +449,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
406
449
  providerKey: block.providerKey,
407
450
  blockScope: block.blockScope,
408
451
  blockedUntilMs: block.blockedUntilMs,
452
+ updatedAtMs: block.updatedAtMs,
409
453
  });
410
454
  }
411
455
  }
@@ -416,6 +460,10 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
416
460
  upsertCredentialBlock(block: StoredCredentialBlock): void {
417
461
  this.#upsertSnapshotBlock(block);
418
462
  this.#invalidateUsageCache();
463
+ this.#credentialBlockReconcileAfter.set(
464
+ `${block.credentialId}\0${block.providerKey}\0${block.blockScope}`,
465
+ Math.min(block.blockedUntilMs, Date.now() + CREDENTIAL_BLOCK_RECONCILE_DELAY_MS),
466
+ );
419
467
  const body = toCredentialBlockSnapshot(block);
420
468
  void this.#client
421
469
  .upsertCredentialBlock(block.credentialId, body)
@@ -434,6 +482,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
434
482
 
435
483
  deleteCredentialBlocks(credentialId: number): void {
436
484
  this.#deleteSnapshotBlocks(credentialId);
485
+ for (const key of this.#credentialBlockReconcileAfter.keys()) {
486
+ if (key.startsWith(`${credentialId}\0`)) this.#credentialBlockReconcileAfter.delete(key);
487
+ }
437
488
  this.#invalidateUsageCache();
438
489
  void this.#client
439
490
  .deleteCredentialBlocks(credentialId)
@@ -450,6 +501,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
450
501
 
451
502
  cleanExpiredCredentialBlocks(nowMs: number): void {
452
503
  this.#pruneExpiredCredentialBlocks(nowMs);
504
+ for (const [key, reconcileAfterMs] of this.#credentialBlockReconcileAfter) {
505
+ if (reconcileAfterMs <= nowMs) this.#credentialBlockReconcileAfter.delete(key);
506
+ }
453
507
  }
454
508
 
455
509
  /**
@@ -647,6 +701,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
647
701
  providerKey: block.providerKey,
648
702
  blockScope: block.blockScope,
649
703
  blockedUntilMs: block.blockedUntilMs,
704
+ ...(block.updatedAtMs !== undefined ? { updatedAtMs: block.updatedAtMs } : {}),
650
705
  }))
651
706
  .sort(compareCredentialBlockSnapshots);
652
707
  if (blocks.length > 0) return { ...entry, blocks };
@@ -290,6 +290,7 @@ function buildCredentialBlockGroups(
290
290
  providerKey: block.providerKey,
291
291
  blockScope: block.blockScope,
292
292
  blockedUntilMs: block.blockedUntilMs,
293
+ updatedAtMs: block.updatedAtMs,
293
294
  };
294
295
  const existing = byCredentialId.get(block.credentialId);
295
296
  if (existing) {
@@ -74,6 +74,7 @@ export const credentialBlockSnapshotSchema = type({
74
74
  providerKey: type("string").atLeastLength(1),
75
75
  blockScope: "string",
76
76
  blockedUntilMs: "number",
77
+ "updatedAtMs?": "number",
77
78
  });
78
79
 
79
80
  export const snapshotEntrySchema = type({
@@ -112,8 +112,8 @@ function deriveSessionId(modelId: string, context: Context): string {
112
112
  parts.push(JSON.stringify({ role: first.role, content: first.content }));
113
113
  }
114
114
  const seed = parts.join("\u0000");
115
- // The 36-char UUID flows through unchanged: Codex's
116
- // `normalizeOpenAIResponsesPromptCacheKey` accepts ≤64 chars verbatim.
115
+ // The 36-char UUID flows through unchanged:
116
+ // `normalizeOpenAIPromptCacheKey` accepts ≤64 chars verbatim.
117
117
  return deterministicUuid(seed);
118
118
  }
119
119