@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.
Files changed (237) hide show
  1. package/CHANGELOG.md +9 -8
  2. package/README.md +15 -0
  3. package/dist/types/auth/affinity.d.ts +33 -6
  4. package/dist/types/auth/cascade.d.ts +44 -7
  5. package/dist/types/auth/oauth.d.ts +9 -6
  6. package/dist/types/auth/policy.d.ts +1 -0
  7. package/dist/types/auth/refresh.d.ts +1 -2
  8. package/dist/types/auth/select.d.ts +6 -4
  9. package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
  10. package/dist/types/auth/store.d.ts +8 -2
  11. package/dist/types/auth/types.d.ts +99 -15
  12. package/dist/types/auth-broker/client.d.ts +3 -3
  13. package/dist/types/auth-broker/discover.d.ts +1 -1
  14. package/dist/types/auth-broker/refresher.d.ts +6 -1
  15. package/dist/types/auth-broker/remote-store.d.ts +6 -4
  16. package/dist/types/auth-broker/server.d.ts +2 -0
  17. package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
  18. package/dist/types/auth-gateway/dispatch.d.ts +13 -5
  19. package/dist/types/auth-gateway/http.d.ts +0 -2
  20. package/dist/types/auth-gateway/index.d.ts +1 -0
  21. package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
  22. package/dist/types/auth-gateway/routes/images.d.ts +3 -3
  23. package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
  24. package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
  25. package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
  26. package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
  27. package/dist/types/auth-gateway/routes/video.d.ts +4 -4
  28. package/dist/types/auth-gateway/server.d.ts +22 -5
  29. package/dist/types/auth-gateway/session-state.d.ts +6 -0
  30. package/dist/types/auth-gateway/stdio.d.ts +12 -0
  31. package/dist/types/auth-storage.d.ts +7 -6
  32. package/dist/types/dialect/terminator-wait.d.ts +19 -0
  33. package/dist/types/env-api-key.d.ts +21 -0
  34. package/dist/types/error/auth.d.ts +10 -0
  35. package/dist/types/error/flags.d.ts +38 -0
  36. package/dist/types/images/index.d.ts +2 -3
  37. package/dist/types/images/shared.d.ts +6 -0
  38. package/dist/types/index.d.ts +2 -1
  39. package/dist/types/judgment/index.d.ts +2 -1
  40. package/dist/types/judgment/native.d.ts +80 -0
  41. package/dist/types/judgment/openai-decisions.d.ts +3 -0
  42. package/dist/types/judgment/question-file.d.ts +52 -0
  43. package/dist/types/judgment/systemone.d.ts +7 -0
  44. package/dist/types/judgment/types.d.ts +2 -2
  45. package/dist/types/judgment/wire.d.ts +36 -0
  46. package/dist/types/provider-details.d.ts +1 -1
  47. package/dist/types/provider-session-state.d.ts +2 -2
  48. package/dist/types/providers/anthropic-client.d.ts +10 -0
  49. package/dist/types/providers/anthropic-state.d.ts +2 -0
  50. package/dist/types/providers/anthropic.d.ts +38 -3
  51. package/dist/types/providers/aws-credentials.d.ts +4 -8
  52. package/dist/types/providers/connect-error-detail.d.ts +9 -0
  53. package/dist/types/providers/connect-frame.d.ts +44 -0
  54. package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
  55. package/dist/types/providers/cursor.d.ts +8 -8
  56. package/dist/types/providers/google-auth.d.ts +4 -1
  57. package/dist/types/providers/google-shared.d.ts +24 -4
  58. package/dist/types/providers/inference-headers.d.ts +6 -0
  59. package/dist/types/providers/mock.d.ts +3 -1
  60. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
  61. package/dist/types/providers/openai-codex-responses.d.ts +1 -1
  62. package/dist/types/providers/openai-configuration-update.d.ts +20 -11
  63. package/dist/types/providers/openai-responses-wire.d.ts +1 -55
  64. package/dist/types/providers/openai-responses.d.ts +40 -3
  65. package/dist/types/providers/openai-shared.d.ts +30 -5
  66. package/dist/types/providers/pi-native-client.d.ts +1 -1
  67. package/dist/types/providers/transform-messages.d.ts +25 -0
  68. package/dist/types/providers/vision-guard.d.ts +1 -1
  69. package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
  70. package/dist/types/registry/oauth/cursor.d.ts +10 -2
  71. package/dist/types/registry/oauth/index.d.ts +2 -0
  72. package/dist/types/registry/oauth/snowflake.d.ts +3 -0
  73. package/dist/types/registry/snowflake.d.ts +4 -0
  74. package/dist/types/stream.d.ts +0 -21
  75. package/dist/types/transcription/types.d.ts +2 -1
  76. package/dist/types/types.d.ts +116 -14
  77. package/dist/types/usage.d.ts +2 -0
  78. package/dist/types/utils/anthropic-auth.d.ts +21 -7
  79. package/dist/types/utils/aws-profile.d.ts +5 -0
  80. package/dist/types/utils/block-symbols.d.ts +14 -0
  81. package/dist/types/utils/dsml-leak.d.ts +29 -0
  82. package/dist/types/utils/glyph-codec.d.ts +1 -1
  83. package/dist/types/utils/idle-iterator.d.ts +11 -0
  84. package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
  85. package/dist/types/utils/resolve-peer.d.ts +2 -0
  86. package/dist/types/utils/retry-wait.d.ts +9 -0
  87. package/dist/types/utils/schema/normalize.d.ts +8 -3
  88. package/dist/types/utils/stream-markup-healing.d.ts +4 -4
  89. package/dist/types/utils/tool-call-arguments.d.ts +8 -0
  90. package/dist/types/utils.d.ts +6 -0
  91. package/package.json +6 -6
  92. package/src/auth/affinity.ts +130 -37
  93. package/src/auth/blocks.ts +94 -15
  94. package/src/auth/cascade.ts +112 -17
  95. package/src/auth/health.ts +3 -2
  96. package/src/auth/oauth.ts +25 -10
  97. package/src/auth/policy.ts +10 -1
  98. package/src/auth/pool.ts +0 -5
  99. package/src/auth/refresh.ts +17 -12
  100. package/src/auth/resets.ts +1 -1
  101. package/src/auth/rotation.ts +8 -2
  102. package/src/auth/select.ts +102 -30
  103. package/src/auth/sqlite-credential-store.ts +119 -49
  104. package/src/auth/store.ts +8 -2
  105. package/src/auth/types.ts +108 -17
  106. package/src/auth/usage-cache.ts +16 -5
  107. package/src/auth/usage.ts +2 -2
  108. package/src/auth-broker/client.ts +7 -4
  109. package/src/auth-broker/discover.ts +6 -5
  110. package/src/auth-broker/refresher.ts +8 -1
  111. package/src/auth-broker/remote-store.ts +200 -62
  112. package/src/auth-broker/server.ts +316 -130
  113. package/src/auth-broker/snapshot-cache.ts +142 -2
  114. package/src/auth-gateway/dispatch.ts +15 -7
  115. package/src/auth-gateway/http.ts +0 -8
  116. package/src/auth-gateway/index.ts +1 -0
  117. package/src/auth-gateway/routes/embeddings.ts +2 -2
  118. package/src/auth-gateway/routes/images.ts +4 -4
  119. package/src/auth-gateway/routes/rerank.ts +2 -2
  120. package/src/auth-gateway/routes/speech.ts +2 -2
  121. package/src/auth-gateway/routes/systemone.ts +22 -14
  122. package/src/auth-gateway/routes/transcriptions.ts +2 -2
  123. package/src/auth-gateway/routes/video.ts +7 -7
  124. package/src/auth-gateway/server.ts +175 -134
  125. package/src/auth-gateway/session-state.ts +51 -9
  126. package/src/auth-gateway/stdio.ts +132 -0
  127. package/src/auth-storage.ts +23 -9
  128. package/src/dialect/deepseek.ts +67 -8
  129. package/src/dialect/gemini.ts +8 -2
  130. package/src/dialect/hermes.ts +8 -2
  131. package/src/dialect/kimi.ts +8 -2
  132. package/src/dialect/qwen-xml.ts +8 -2
  133. package/src/dialect/qwen3.ts +8 -2
  134. package/src/dialect/terminator-wait.ts +42 -0
  135. package/src/env-api-key.ts +78 -0
  136. package/src/error/auth.ts +20 -0
  137. package/src/error/flags.ts +143 -24
  138. package/src/error/rate-limit.ts +8 -6
  139. package/src/images/google-antigravity.ts +0 -1
  140. package/src/images/index.ts +3 -17
  141. package/src/images/openai-hosted.ts +13 -4
  142. package/src/images/openai-images.ts +19 -13
  143. package/src/images/shared.ts +10 -2
  144. package/src/index.ts +2 -1
  145. package/src/judgment/index.ts +2 -1
  146. package/src/judgment/native.ts +333 -0
  147. package/src/judgment/openai-decisions-predicate.md +9 -0
  148. package/src/judgment/openai-decisions.ts +129 -0
  149. package/src/judgment/question-file.ts +90 -0
  150. package/src/judgment/systemone.ts +65 -0
  151. package/src/judgment/text.ts +9 -1
  152. package/src/judgment/types.ts +2 -2
  153. package/src/judgment/wire.ts +42 -0
  154. package/src/provider-details.ts +1 -1
  155. package/src/provider-session-state.ts +2 -2
  156. package/src/providers/amazon-bedrock.ts +19 -8
  157. package/src/providers/anthropic-client.ts +16 -1
  158. package/src/providers/anthropic-state.ts +6 -0
  159. package/src/providers/anthropic.ts +962 -238
  160. package/src/providers/apple-foundation-models.ts +10 -4
  161. package/src/providers/aws-credentials.ts +65 -5
  162. package/src/providers/azure-openai-responses.ts +1 -1
  163. package/src/providers/connect-error-detail.ts +21 -0
  164. package/src/providers/connect-frame.ts +107 -0
  165. package/src/providers/cursor/interaction-query.ts +1 -9
  166. package/src/providers/cursor.ts +590 -133
  167. package/src/providers/devin.ts +68 -58
  168. package/src/providers/factory-droid/gemini.ts +38 -51
  169. package/src/providers/factory-droid.ts +2 -0
  170. package/src/providers/gitlab-duo-workflow.ts +5 -2
  171. package/src/providers/gitlab-duo.ts +1 -0
  172. package/src/providers/google-auth.ts +10 -3
  173. package/src/providers/google-gemini-cli.ts +4 -0
  174. package/src/providers/google-shared.ts +134 -105
  175. package/src/providers/google-vertex.ts +0 -1
  176. package/src/providers/google.ts +1 -1
  177. package/src/providers/inference-headers.ts +19 -0
  178. package/src/providers/mock.ts +4 -0
  179. package/src/providers/ollama.ts +1 -1
  180. package/src/providers/openai-anthropic-shim.ts +1 -0
  181. package/src/providers/openai-codex/request-transformer.ts +16 -11
  182. package/src/providers/openai-codex-responses.ts +201 -76
  183. package/src/providers/openai-completions.ts +12 -2
  184. package/src/providers/openai-configuration-update.ts +54 -15
  185. package/src/providers/openai-responses-wire.ts +1 -55
  186. package/src/providers/openai-responses.ts +437 -33
  187. package/src/providers/openai-shared.ts +267 -72
  188. package/src/providers/pi-native-client.ts +2 -2
  189. package/src/providers/pi-native-server.ts +1 -0
  190. package/src/providers/systemone-server.ts +1 -1
  191. package/src/providers/transcriptions-server.ts +1 -1
  192. package/src/providers/transform-messages.ts +240 -84
  193. package/src/providers/vision-guard.ts +1 -1
  194. package/src/registry/aws.ts +51 -14
  195. package/src/registry/cloudflare-ai-gateway.ts +26 -3
  196. package/src/registry/hooks/api-key.ts +1 -0
  197. package/src/registry/hooks/custom.ts +2 -0
  198. package/src/registry/hooks/env.ts +2 -5
  199. package/src/registry/oauth/callback-server.ts +6 -1
  200. package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
  201. package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
  202. package/src/registry/oauth/cursor.ts +80 -17
  203. package/src/registry/oauth/index.ts +5 -0
  204. package/src/registry/oauth/snowflake.ts +183 -0
  205. package/src/registry/registry.ts +2 -0
  206. package/src/registry/snowflake.ts +149 -0
  207. package/src/stream.ts +85 -107
  208. package/src/transcription/types.ts +2 -1
  209. package/src/types.ts +142 -15
  210. package/src/usage/claude.ts +19 -12
  211. package/src/usage/commandcode.ts +1 -1
  212. package/src/usage/cursor.ts +14 -25
  213. package/src/usage/github-copilot.ts +1 -1
  214. package/src/usage/kimi.ts +1 -1
  215. package/src/usage/minimax-code.ts +8 -1
  216. package/src/usage.ts +8 -0
  217. package/src/utils/anthropic-auth.ts +34 -10
  218. package/src/utils/aws-profile.ts +10 -4
  219. package/src/utils/block-symbols.ts +17 -0
  220. package/src/utils/dsml-leak.ts +109 -0
  221. package/src/utils/empty-completion-retry.ts +2 -3
  222. package/src/utils/glyph-codec.ts +200 -7
  223. package/src/utils/idle-iterator.ts +80 -35
  224. package/src/utils/leaked-thinking-stream.ts +47 -25
  225. package/src/utils/resolve-peer.ts +7 -0
  226. package/src/utils/retry-wait.ts +19 -0
  227. package/src/utils/schema/CONSTRAINTS.md +9 -7
  228. package/src/utils/schema/fields.ts +20 -0
  229. package/src/utils/schema/normalize.ts +65 -23
  230. package/src/utils/schema/wire.ts +3 -3
  231. package/src/utils/stream-markup-healing.ts +4 -4
  232. package/src/utils/thinking-loop.ts +36 -13
  233. package/src/utils/tool-call-arguments.ts +16 -0
  234. package/src/utils/transport-fetch.ts +1 -1
  235. package/src/utils.ts +41 -10
  236. package/dist/types/judgment/typesafe.d.ts +0 -49
  237. package/src/judgment/typesafe.ts +0 -183
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Removal of DeepSeek DSML tool-call markup left in visible assistant text.
3
+ *
4
+ * DeepSeek models on OpenAI-compatible hosts sometimes emit their tool calls as
5
+ * raw `<|DSML|…>` markup in `content`. A well-formed envelope is healed into a
6
+ * structured call by `StreamMarkupHealing`, but a degraded model can drop the
7
+ * opening `<|DSML|tool_calls>` / `<|DSML|invoke name="…">` tags (or the host
8
+ * runs no healer at all). That markup must not stay in history: replaying it
9
+ * teaches the model to keep writing tool calls as text (issue #10556). Callers
10
+ * strip it before the message is committed and tell the model its call failed.
11
+ */
12
+ import type { AssistantMessage, Model } from "../types.js";
13
+ /**
14
+ * Whether DSML leak recovery applies to `model`: a DeepSeek-class model, or one
15
+ * explicitly configured with the `dsml` stream healer (e.g. a `models.yml` entry
16
+ * whose id the classifier cannot place). Other models never write DSML tool
17
+ * calls, so DSML text from them is prose and is left alone.
18
+ */
19
+ export declare function isDsmlLeakRecoveryTarget(model: Model): boolean;
20
+ /**
21
+ * Strip leaked DSML tool-call markup from `text`, leaving quoted code intact.
22
+ * Returns `undefined` when `text` carries no such markup.
23
+ */
24
+ export declare function stripDsmlToolMarkup(text: string): string | undefined;
25
+ /**
26
+ * Remove leaked DSML tool-call markup from `message`'s text blocks in place,
27
+ * dropping blocks left empty. Returns true when anything was removed.
28
+ */
29
+ export declare function removeDsmlToolMarkupLeak(message: AssistantMessage): boolean;
@@ -4,7 +4,7 @@ import { AssistantMessageEventStream } from "./event-stream.js";
4
4
  export interface GlyphCodec {
5
5
  /** Context with model-bound glyphs encoded and one conditional convention notice. */
6
6
  context: Context;
7
- /** Decodes glyph tokens in the stream's terminal assistant message. */
7
+ /** Decodes glyph tokens live: text and tool-call deltas, partial snapshots, completed tool calls, and the terminal message. */
8
8
  wrap(inner: AssistantMessageEventStream): AssistantMessageEventStream;
9
9
  /** Decodes Cursor Pi calls and encodes their provider-bound results. */
10
10
  wrapCursorExecHandlers(handlers: CursorExecHandlers): CursorExecHandlers;
@@ -138,6 +138,12 @@ export interface TerminalGraceIteratorOptions {
138
138
  * the transport until that pending read settles.
139
139
  */
140
140
  onGraceEnd?: () => void;
141
+ /**
142
+ * Wait for the bounded drain when the consumer returns early. Capped
143
+ * requests must not release their in-flight permit while the transport
144
+ * still owns a live connection.
145
+ */
146
+ awaitDrainOnReturn?: boolean;
141
147
  }
142
148
  /**
143
149
  * Yields items from an async iterable until the consumer marks the stream
@@ -148,5 +154,10 @@ export interface TerminalGraceIteratorOptions {
148
154
  * hangs on `iterator.next()` until the idle watchdog converts an
149
155
  * already-successful turn into a timeout error. Grace expiry is a clean end
150
156
  * of iteration, never an error.
157
+ *
158
+ * A consumer that stops early (`break`) after `finishedAtMs()` is set leaves
159
+ * the source draining until `[DONE]`/EOF or the same grace deadline. Without
160
+ * `awaitDrainOnReturn`, the consumer is not held up; capped requests await
161
+ * the drain before completing and releasing their in-flight permit.
151
162
  */
152
163
  export declare function iterateWithTerminalGrace<T>(iterable: AsyncIterable<T>, options: TerminalGraceIteratorOptions): AsyncGenerator<T>;
@@ -23,7 +23,7 @@
23
23
  * fences, so wrapping a provider that already heals (or wrapping twice) is a
24
24
  * harmless pass-through. Signatures are load-bearing for Google/Gemini/Vertex
25
25
  * thought round-tripping, so text sub-blocks carry the source `textSignature`,
26
- * forwarded thinking blocks their `thinkingSignature`, and forwarded tool calls
26
+ * forwarded thinking blocks their `thinkingSignature` and `summary` state, and forwarded tool calls
27
27
  * their `thoughtSignature`.
28
28
  *
29
29
  * Modeled on {@link wrapInbandToolStream} / `InbandStreamProjector` in
@@ -0,0 +1,2 @@
1
+ /** Use the socket peer unless the server explicitly trusts its reverse proxy. */
2
+ export declare function resolvePeer(req: Request, socketAddress: string, trustProxyHeaders?: boolean): string;
@@ -0,0 +1,9 @@
1
+ /** Injected retry sleep: `StreamOptions.providerRetryWait` / `UsageFetchContext.retryWait`. */
2
+ export type RetryWaitHook = (delayMs: number, signal?: AbortSignal) => Promise<void>;
3
+ /**
4
+ * Waits `delayMs` between retry attempts. Honors the caller's retry-wait hook
5
+ * when one is set (tests and custom transports inject it to avoid real sleeps)
6
+ * and otherwise sleeps on `signal`, so an abort rejects instead of the retry
7
+ * proceeding.
8
+ */
9
+ export declare function waitForRetry(retryWait: RetryWaitHook | undefined, delayMs: number, signal?: AbortSignal): Promise<void>;
@@ -10,7 +10,11 @@ export interface NormalizeSchemaOptions {
10
10
  unsupportedFields: (key: string) => boolean;
11
11
  normalizeFieldNames: boolean;
12
12
  collapseNullFields: boolean;
13
- normalizeTypeArrayToNullable: boolean;
13
+ /**
14
+ * Target takes a scalar `type` only: `["T", "null"]` becomes `T` (+ `nullable`)
15
+ * and a multi-type array becomes one `anyOf` branch per type.
16
+ */
17
+ scalarizeTypeArrays: boolean;
14
18
  stripNullableKeyword: boolean;
15
19
  autoPropertyOrdering: boolean;
16
20
  ensureObjectProperties: boolean;
@@ -66,8 +70,9 @@ export declare function normalizeSchemaForMCP(value: unknown): unknown;
66
70
  * rejected; collapse to `enum` with an inferred scalar `type`.
67
71
  * - `oneOf` is not an MFJS combinator (only `anyOf` is); residual `oneOf` is
68
72
  * folded into `anyOf`.
69
- * - `type` must be a scalar string; `type: [...]` arrays are reduced to a
70
- * single scalar (the `null` branch is dropped — `nullable` is unsupported).
73
+ * - `type` must be a scalar string; `["T", "null"]` reduces to `T` (the
74
+ * `null` member is dropped — `nullable` is unsupported) and a multi-type
75
+ * array becomes one typed `anyOf` branch per type.
71
76
  * - Enum-bearing nodes get an inferred `type` (the idiomatic MFJS form; a bare
72
77
  * `enum` is valid too) so `anyOf` branches always carry a `type`.
73
78
  * - Validation/decorative keywords (`minItems`, `maxItems`, `maxLength`,
@@ -63,10 +63,10 @@ export declare class StreamMarkupHealing {
63
63
  /** Drain accumulated tool calls from calls to {@link feed}. */
64
64
  drainCompleted(): HealedToolCall[];
65
65
  /**
66
- * Flush held-back stream-end fragments as ordered events. Partial tool-call
67
- * sections/envelopes are dropped by the delegated scanners; unterminated
68
- * thinking blocks are emitted as thinking, matching the previous MiniMax parser
69
- * behavior.
66
+ * Flush held-back stream-end fragments as ordered events. Partial tool calls
67
+ * are dropped by the delegated scanners; a DSML wrapper without an invoke
68
+ * remains visible for agent-layer leak recovery. Unterminated thinking blocks
69
+ * are emitted as thinking.
70
70
  */
71
71
  flushEvents(): StreamMarkupHealingEvent[];
72
72
  /** Flush held-back text only. Reconstructed calls are retained for {@link drainCompleted}. */
@@ -1,6 +1,14 @@
1
1
  import type { ToolCall } from "../types.js";
2
2
  /** Final arguments must not reuse the streaming parser's auto-closed preview. */
3
3
  export declare function parseToolCallArguments(json: string | undefined): ToolCall["arguments"];
4
+ /**
5
+ * Native Responses `function_call.arguments` to persist for replay.
6
+ *
7
+ * {@link parseToolCallArguments} repairs some invalid JSON (e.g. `"name": ,`) and the tool runs with the
8
+ * repaired value; replay drops a call whose stored arguments do not parse, orphaning its output (#14155).
9
+ * Returns `raw` when it already parses or the arguments were unrepairable, else the executed arguments.
10
+ */
11
+ export declare function replayableToolCallArguments(raw: string, executed: ToolCall["arguments"]): string;
4
12
  /** Longest raw argument text kept in the parse-error diagnostic. */
5
13
  export declare const INVALID_ARGUMENTS_RAW_LIMIT = 512;
6
14
  /** Bound raw argument text for a diagnostic. Text this function already bounded is returned unchanged. */
@@ -34,6 +34,12 @@ export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem ex
34
34
  type?: unknown;
35
35
  status?: unknown;
36
36
  }>(items: TItem[]): TItem[];
37
+ /** Clamp replayed images without changing item identities or other native replay fields. */
38
+ export declare function clampOpenAIResponsesImageDetailForReplay<TItem extends {
39
+ type?: unknown;
40
+ detail?: unknown;
41
+ content?: unknown;
42
+ }>(items: TItem[], supportsImageDetailOriginal: boolean): TItem[];
37
43
  export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput;
38
44
  /** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
39
45
  export declare function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-ai",
3
- "version": "1.1.26",
3
+ "version": "1.2.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -152,11 +152,11 @@
152
152
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
153
153
  },
154
154
  "dependencies": {
155
- "@linxiraos/pi-omptype": "1.1.26",
156
- "@linxiraos/pi-catalog": "1.1.26",
157
- "@linxiraos/pi-natives": "1.1.26",
158
- "@linxiraos/pi-utils": "1.1.26",
159
- "@linxiraos/pi-wire": "1.1.26"
155
+ "@linxiraos/pi-omptype": "1.2.0",
156
+ "@linxiraos/pi-catalog": "1.2.0",
157
+ "@linxiraos/pi-natives": "1.2.0",
158
+ "@linxiraos/pi-utils": "1.2.0",
159
+ "@linxiraos/pi-wire": "1.2.0"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@types/bun": "^1.3.14"
@@ -1,9 +1,11 @@
1
1
  import { logger } from "@linxiraos/pi-utils";
2
- import { getEnvApiKey } from "../stream";
3
- import type { AuthCredential, OAuthCredential, SessionsApi } from "./types";
2
+ import { LRUCache } from "@linxiraos/pi-utils/lru";
3
+ import { getEnvApiKey } from "../env-api-key";
4
+ import type { AuthCredential, OAuthCredential, SessionRestrictionLease, SessionsApi } from "./types";
4
5
  import type { AuthCredentialStore } from "./store";
5
6
  import type { CredentialPool } from "./pool";
6
7
  import type { KeyOverrides } from "./cascade";
8
+ import { resolveCredentialIdentityKey } from "./sqlite-credential-store";
7
9
 
8
10
  /** Prefix for persisted session-to-credential affinity. */
9
11
  export const SESSION_STICKY_CACHE_PREFIX = "session:sticky:";
@@ -14,6 +16,11 @@ const SESSION_STICKY_TTL_SEC = 30 * 24 * 60 * 60;
14
16
  * in-memory sticky stays exact; only the copy other processes resume from lags.
15
17
  */
16
18
  const SESSION_STICKY_PERSIST_INTERVAL_MS = 60_000;
19
+ /**
20
+ * In-memory pins kept per provider. Long-lived gateways mint a session id per
21
+ * conversation; evicted sessions resume from their persisted sticky row.
22
+ */
23
+ const SESSION_AFFINITY_MAX_SESSIONS_PER_PROVIDER = 256;
17
24
 
18
25
  /** What this process last wrote (or read) for one persisted sticky row. */
19
26
  type PersistedSticky = {
@@ -33,29 +40,100 @@ export type SessionCredential = {
33
40
  explicit?: true;
34
41
  };
35
42
 
43
+ /** One installed restriction: the allowed OAuth identity keys and the lease that lifts it. */
44
+ type SessionRestriction = { readonly allowed: ReadonlySet<string>; readonly lease: SessionRestrictionLease };
45
+
46
+ /**
47
+ * Provider → session id → installed restriction. Owned by `AuthStorage` so
48
+ * restrictions outlive store replacement, which rebuilds every store-bound
49
+ * module (pins included) around the same identities.
50
+ */
51
+ export type SessionRestrictions = Map<string, Map<string, SessionRestriction>>;
52
+
36
53
  /** Session → credential affinity (pins), persisted in the store cache. */
37
54
  export class SessionAffinity implements SessionsApi {
38
55
  /** Tracks the last used credential per provider for a session (used for rate-limit switching). */
39
- #sessionLastCredential: Map<string, Map<string, SessionCredential>> = new Map();
40
- /** Persisted sticky rows keyed by cache key, so unchanged re-records skip the write. */
41
- #persistedSticky: Map<string, PersistedSticky> = new Map();
56
+ #sessionLastCredential: Map<string, LRUCache<string, SessionCredential>> = new Map();
57
+ /** Persisted sticky rows per provider, keyed by session id, so unchanged re-records skip the write. */
58
+ #persistedSticky: Map<string, LRUCache<string, PersistedSticky>> = new Map();
42
59
  #store: AuthCredentialStore;
43
60
  #pool: CredentialPool;
44
61
  #overrides: KeyOverrides;
62
+ #restrictions: SessionRestrictions;
45
63
 
46
- constructor(store: AuthCredentialStore, pool: CredentialPool, overrides: KeyOverrides) {
64
+ constructor(
65
+ store: AuthCredentialStore,
66
+ pool: CredentialPool,
67
+ overrides: KeyOverrides,
68
+ restrictions: SessionRestrictions,
69
+ ) {
47
70
  this.#store = store;
48
71
  this.#pool = pool;
49
72
  this.#overrides = overrides;
73
+ this.#restrictions = restrictions;
74
+ }
75
+
76
+ restrict(provider: string, sessionId: string, identityKeys: readonly string[]): SessionRestrictionLease {
77
+ if (!provider || !sessionId) throw new Error("sessions.restrict requires a provider and a session id");
78
+ const lease = Symbol("sessions.restrict");
79
+ const sessions = this.#restrictions.get(provider) ?? new Map<string, SessionRestriction>();
80
+ sessions.set(sessionId, { allowed: new Set(identityKeys), lease });
81
+ this.#restrictions.set(provider, sessions);
82
+ return lease;
83
+ }
84
+
85
+ unrestrict(provider: string, sessionId: string, lease: SessionRestrictionLease): void {
86
+ const sessions = this.#restrictions.get(provider);
87
+ if (sessions?.get(sessionId)?.lease !== lease) return;
88
+ sessions.delete(sessionId);
89
+ if (sessions.size === 0) this.#restrictions.delete(provider);
90
+ }
91
+
92
+ /** True when {@link restrict} limits `sessionId` for `provider`. */
93
+ isRestricted(provider: string, sessionId: string | undefined): boolean {
94
+ return sessionId !== undefined && this.#restrictions.get(provider)?.has(sessionId) === true;
95
+ }
96
+
97
+ /**
98
+ * True when `credential` may serve `sessionId`: always for an unrestricted
99
+ * session, otherwise only an OAuth credential whose identity key is allowed.
100
+ */
101
+ allows(provider: string, sessionId: string | undefined, credential: AuthCredential): boolean {
102
+ const allowed = sessionId === undefined ? undefined : this.#restrictions.get(provider)?.get(sessionId)?.allowed;
103
+ if (allowed === undefined) return true;
104
+ if (credential.type !== "oauth") return false;
105
+ const identityKey = resolveCredentialIdentityKey(provider, credential);
106
+ return identityKey !== null && allowed.has(identityKey);
107
+ }
108
+
109
+ /** Whether the stored row at `index` may serve `sessionId` (see {@link allows}). */
110
+ #permits(provider: string, sessionId: string, index: number): boolean {
111
+ if (!this.isRestricted(provider, sessionId)) return true;
112
+ const credential = this.#pool.credentials(provider)[index];
113
+ return credential !== undefined && this.allows(provider, sessionId, credential);
114
+ }
115
+
116
+ permits(provider: string, sessionId: string, credentialId: number): boolean {
117
+ if (!this.isRestricted(provider, sessionId)) return true;
118
+ const entry = this.#pool.entries(provider).find(candidate => candidate.id === credentialId);
119
+ return entry !== undefined && this.allows(provider, sessionId, entry.credential);
120
+ }
121
+
122
+ /** Bounded per-provider session map, created on first use. */
123
+ static #sessionsFor<V>(maps: Map<string, LRUCache<string, V>>, provider: string): LRUCache<string, V> {
124
+ let sessions = maps.get(provider);
125
+ if (!sessions) {
126
+ sessions = new LRUCache<string, V>({ max: SESSION_AFFINITY_MAX_SESSIONS_PER_PROVIDER });
127
+ maps.set(provider, sessions);
128
+ }
129
+ return sessions;
50
130
  }
51
131
 
52
132
  /** Drop every pin for a provider, both in memory and in the persisted cache. */
53
133
  clearProvider(provider: string): void {
54
134
  this.#sessionLastCredential.delete(provider);
135
+ this.#persistedSticky.delete(provider);
55
136
  const prefix = `${SESSION_STICKY_CACHE_PREFIX}${provider}:`;
56
- for (const cacheKey of this.#persistedSticky.keys()) {
57
- if (cacheKey.startsWith(prefix)) this.#persistedSticky.delete(cacheKey);
58
- }
59
137
  try {
60
138
  this.#store.deleteCachePrefix?.(prefix);
61
139
  } catch (err) {
@@ -87,10 +165,10 @@ export class SessionAffinity implements SessionsApi {
87
165
  lastUsedAtMs?: number,
88
166
  explicit = false,
89
167
  ): void {
90
- if (!sessionId) return;
168
+ if (!sessionId || !this.#permits(provider, sessionId, index)) return;
91
169
  const nowMs = lastUsedAtMs ?? Date.now();
92
170
  const credentialId = this.#pool.entries(provider)[index]?.id;
93
- const sessionMap = this.#sessionLastCredential.get(provider) ?? new Map();
171
+ const sessionMap = SessionAffinity.#sessionsFor(this.#sessionLastCredential, provider);
94
172
  const previous = sessionMap.get(sessionId);
95
173
  const sameCredential =
96
174
  previous?.type === type &&
@@ -104,12 +182,12 @@ export class SessionAffinity implements SessionsApi {
104
182
  ...(isExplicit ? { explicit: true as const } : {}),
105
183
  };
106
184
  sessionMap.set(sessionId, sessionCredential);
107
- this.#sessionLastCredential.set(provider, sessionMap);
108
185
 
109
186
  if (credentialId === undefined) return;
110
187
  const cacheKey = `${SESSION_STICKY_CACHE_PREFIX}${provider}:${sessionId}`;
111
188
  const expiresAtSec = Math.floor(nowMs / 1000) + SESSION_STICKY_TTL_SEC;
112
- const persisted = this.#persistedSticky.get(cacheKey);
189
+ const persistedSessions = SessionAffinity.#sessionsFor(this.#persistedSticky, provider);
190
+ const persisted = persistedSessions.get(sessionId);
113
191
  if (
114
192
  persisted &&
115
193
  persisted.type === type &&
@@ -121,23 +199,31 @@ export class SessionAffinity implements SessionsApi {
121
199
  }
122
200
  try {
123
201
  this.#store.setCache(cacheKey, JSON.stringify(sessionCredential), expiresAtSec);
124
- this.#persistedSticky.set(cacheKey, {
202
+ persistedSessions.set(sessionId, {
125
203
  type,
126
204
  credentialId,
127
205
  explicit: isExplicit,
128
206
  lastUsedAtMs: nowMs,
129
207
  });
130
208
  } catch (err) {
131
- this.#persistedSticky.delete(cacheKey);
209
+ persistedSessions.delete(sessionId);
132
210
  logger.debug("Failed to write session sticky credential to persistent store cache", { err });
133
211
  }
134
212
  }
135
213
 
136
- /** Retrieves the last credential used by a session. */
214
+ /**
215
+ * Retrieves the last credential used by a session. A restricted session
216
+ * never sees a pin outside its allowlist — inherited, restored, or recorded
217
+ * before the restriction — so it re-ranks inside the allowlist instead.
218
+ */
137
219
  get(provider: string, sessionId: string | undefined): SessionCredential | undefined {
138
220
  if (!sessionId) return undefined;
139
- let sessionMap = this.#sessionLastCredential.get(provider);
140
- const live = sessionMap?.get(sessionId);
221
+ const credential = this.#lookup(provider, sessionId);
222
+ return credential && this.#permits(provider, sessionId, credential.index) ? credential : undefined;
223
+ }
224
+
225
+ #lookup(provider: string, sessionId: string): SessionCredential | undefined {
226
+ const live = this.#sessionLastCredential.get(provider)?.get(sessionId);
141
227
  if (live) {
142
228
  // Another process can add or drop rows mid-session and the pool is an
143
229
  // index-ordered snapshot, so re-resolve the pin through its durable row
@@ -147,7 +233,7 @@ export class SessionAffinity implements SessionsApi {
147
233
  const stored = this.#pool.entries(provider);
148
234
  const actualIndex = stored.findIndex(entry => entry.id === live.credentialId);
149
235
  if (actualIndex === -1 || stored[actualIndex]?.credential.type !== live.type) {
150
- sessionMap?.delete(sessionId);
236
+ this.#sessionLastCredential.get(provider)?.delete(sessionId);
151
237
  return undefined;
152
238
  }
153
239
  live.index = actualIndex;
@@ -163,22 +249,18 @@ export class SessionAffinity implements SessionsApi {
163
249
  const stored = this.#pool.entries(provider);
164
250
  const actualIndex = stored.findIndex(entry => entry.id === val.credentialId);
165
251
  if (actualIndex === -1 || stored[actualIndex]?.credential.type !== val.type) {
166
- this.#persistedSticky.delete(cacheKey);
252
+ this.#persistedSticky.get(provider)?.delete(sessionId);
167
253
  this.#store.setCache(cacheKey, "", 0);
168
254
  return undefined;
169
255
  }
170
256
  val.index = actualIndex;
171
257
  } else {
172
258
  // Fallback: drop unsafe index-only cache rows to prevent wrong-account routing
173
- this.#persistedSticky.delete(cacheKey);
259
+ this.#persistedSticky.get(provider)?.delete(sessionId);
174
260
  this.#store.setCache(cacheKey, "", 0);
175
261
  return undefined;
176
262
  }
177
263
 
178
- if (!sessionMap) {
179
- sessionMap = new Map();
180
- this.#sessionLastCredential.set(provider, sessionMap);
181
- }
182
264
  const sessionVal: SessionCredential = {
183
265
  type: val.type,
184
266
  index: val.index,
@@ -187,14 +269,14 @@ export class SessionAffinity implements SessionsApi {
187
269
  ...(val.explicit === true ? { explicit: true } : {}),
188
270
  };
189
271
  if (typeof val.lastUsedAtMs === "number") {
190
- this.#persistedSticky.set(cacheKey, {
272
+ SessionAffinity.#sessionsFor(this.#persistedSticky, provider).set(sessionId, {
191
273
  type: val.type,
192
274
  credentialId: val.credentialId,
193
275
  explicit: val.explicit === true,
194
276
  lastUsedAtMs: val.lastUsedAtMs,
195
277
  });
196
278
  }
197
- sessionMap.set(sessionId, sessionVal);
279
+ SessionAffinity.#sessionsFor(this.#sessionLastCredential, provider).set(sessionId, sessionVal);
198
280
  return sessionVal;
199
281
  }
200
282
  } catch (err) {
@@ -214,7 +296,7 @@ export class SessionAffinity implements SessionsApi {
214
296
  }
215
297
  }
216
298
  const cacheKey = `${SESSION_STICKY_CACHE_PREFIX}${provider}:${sessionId}`;
217
- this.#persistedSticky.delete(cacheKey);
299
+ this.#persistedSticky.get(provider)?.delete(sessionId);
218
300
  try {
219
301
  this.#store.setCache(cacheKey, "", 0);
220
302
  } catch (err) {
@@ -224,12 +306,17 @@ export class SessionAffinity implements SessionsApi {
224
306
 
225
307
  activeOAuth(provider: string, sessionId?: string): OAuthCredential | undefined {
226
308
  const allCredentials = this.#pool.credentials(provider);
227
- const oauthCredentials = allCredentials.filter((c): c is OAuthCredential => c.type === "oauth");
309
+ const oauthCredentials = allCredentials.filter(
310
+ (c): c is OAuthCredential => c.type === "oauth" && this.allows(provider, sessionId, c),
311
+ );
228
312
  if (oauthCredentials.length === 0) return undefined;
229
313
 
230
314
  // Runtime / config overrides bypass OAuth account_uuid attribution — the
231
315
  // caller is authenticating with an explicit key, not the broker's OAuth.
232
- if (this.#overrides.has(provider)) return undefined;
316
+ // A restricted session skips a runtime key, and a config key fails it
317
+ // closed (see `KeyCascade.get`).
318
+ const restricted = this.isRestricted(provider, sessionId);
319
+ if (this.#overrides.suppressesOAuth(provider, restricted)) return undefined;
233
320
 
234
321
  // Prefer the session-sticky credential when available.
235
322
  const sessionPref = this.get(provider, sessionId);
@@ -242,7 +329,7 @@ export class SessionAffinity implements SessionsApi {
242
329
  // would misattribute traffic. Only apply this guard when sessionPref is absent; a
243
330
  // recorded OAuth sticky (sessionPref.type === "oauth") must NOT be blocked even if an
244
331
  // env key also happens to exist.
245
- if (!sessionPref && getEnvApiKey(provider)) return undefined;
332
+ if (!restricted && !sessionPref && getEnvApiKey(provider)) return undefined;
246
333
  // Resolve the sticky index against the full credential list — the index is
247
334
  // recorded against the unfiltered provider array (by record /
248
335
  // CredentialSelector.tryOAuth), not the OAuth-only subset, so dereferencing it into the
@@ -266,32 +353,38 @@ export class SessionAffinity implements SessionsApi {
266
353
  * account, a stale resume re-ranks.
267
354
  */
268
355
  pin(provider: string, sessionId: string, credentialId: number, options?: { restoredAtMs?: number }): boolean {
269
- if (!sessionId || this.#overrides.has(provider)) {
356
+ if (!sessionId || this.#overrides.suppressesOAuth(provider, this.isRestricted(provider, sessionId))) {
270
357
  return false;
271
358
  }
272
359
  const stored = this.#pool.entries(provider);
273
360
  const index = stored.findIndex(entry => entry.id === credentialId);
274
361
  const target = stored[index];
275
- if (target?.credential.type !== "oauth") return false;
362
+ if (target?.credential.type !== "oauth" || !this.allows(provider, sessionId, target.credential)) return false;
276
363
  const restoredAtMs = options?.restoredAtMs;
277
364
  this.record(provider, sessionId, "oauth", index, restoredAtMs, restoredAtMs === undefined);
278
365
  return true;
279
366
  }
280
367
 
281
368
  /**
282
- * Copy every stored credential affinity from one live session to another.
369
+ * Copy stored credential affinities from one live session to another.
283
370
  *
284
371
  * The target receives its own sticky entries, so request resolution, usage
285
372
  * blocking, credential rotation, metadata, and persisted pins all continue
286
373
  * through the target session id without retaining a live dependency on the
287
- * source session.
374
+ * source session. `include` limits the copy to the providers it accepts,
375
+ * given whether the source's affinity is an explicit user pin.
288
376
  */
289
- inherit(sourceSessionId: string, targetSessionId: string): number {
377
+ inherit(
378
+ sourceSessionId: string,
379
+ targetSessionId: string,
380
+ include?: (provider: string, explicit: boolean) => boolean,
381
+ ): number {
290
382
  if (!sourceSessionId || !targetSessionId || sourceSessionId === targetSessionId) return 0;
291
383
  let inherited = 0;
292
384
  for (const provider of this.#pool.providers()) {
293
385
  const credential = this.get(provider, sourceSessionId);
294
- if (!credential) continue;
386
+ if (!credential || !this.#permits(provider, targetSessionId, credential.index)) continue;
387
+ if (include && !include(provider, credential.explicit === true)) continue;
295
388
  this.record(
296
389
  provider,
297
390
  targetSessionId,