@linxiraos/pi-ai 1.1.26 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -8
- package/README.md +15 -0
- package/dist/types/auth/affinity.d.ts +33 -6
- package/dist/types/auth/cascade.d.ts +44 -7
- package/dist/types/auth/oauth.d.ts +9 -6
- package/dist/types/auth/policy.d.ts +1 -0
- package/dist/types/auth/refresh.d.ts +1 -2
- package/dist/types/auth/select.d.ts +6 -4
- package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
- package/dist/types/auth/store.d.ts +8 -2
- package/dist/types/auth/types.d.ts +99 -15
- package/dist/types/auth-broker/client.d.ts +3 -3
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -4
- package/dist/types/auth-broker/server.d.ts +2 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/dispatch.d.ts +13 -5
- package/dist/types/auth-gateway/http.d.ts +0 -2
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
- package/dist/types/auth-gateway/routes/images.d.ts +3 -3
- package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
- package/dist/types/auth-gateway/routes/video.d.ts +4 -4
- package/dist/types/auth-gateway/server.d.ts +22 -5
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/stdio.d.ts +12 -0
- package/dist/types/auth-storage.d.ts +7 -6
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -0
- package/dist/types/error/auth.d.ts +10 -0
- package/dist/types/error/flags.d.ts +38 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/images/shared.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/judgment/index.d.ts +2 -1
- package/dist/types/judgment/native.d.ts +80 -0
- package/dist/types/judgment/openai-decisions.d.ts +3 -0
- package/dist/types/judgment/question-file.d.ts +52 -0
- package/dist/types/judgment/systemone.d.ts +7 -0
- package/dist/types/judgment/types.d.ts +2 -2
- package/dist/types/judgment/wire.d.ts +36 -0
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-client.d.ts +10 -0
- package/dist/types/providers/anthropic-state.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +38 -3
- package/dist/types/providers/aws-credentials.d.ts +4 -8
- package/dist/types/providers/connect-error-detail.d.ts +9 -0
- package/dist/types/providers/connect-frame.d.ts +44 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
- package/dist/types/providers/cursor.d.ts +8 -8
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/google-shared.d.ts +24 -4
- package/dist/types/providers/inference-headers.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-configuration-update.d.ts +20 -11
- package/dist/types/providers/openai-responses-wire.d.ts +1 -55
- package/dist/types/providers/openai-responses.d.ts +40 -3
- package/dist/types/providers/openai-shared.d.ts +30 -5
- package/dist/types/providers/pi-native-client.d.ts +1 -1
- package/dist/types/providers/transform-messages.d.ts +25 -0
- package/dist/types/providers/vision-guard.d.ts +1 -1
- package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
- package/dist/types/registry/oauth/cursor.d.ts +10 -2
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/types.d.ts +116 -14
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +21 -7
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/dist/types/utils/block-symbols.d.ts +14 -0
- package/dist/types/utils/dsml-leak.d.ts +29 -0
- package/dist/types/utils/glyph-codec.d.ts +1 -1
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
- package/dist/types/utils/resolve-peer.d.ts +2 -0
- package/dist/types/utils/retry-wait.d.ts +9 -0
- package/dist/types/utils/schema/normalize.d.ts +8 -3
- package/dist/types/utils/stream-markup-healing.d.ts +4 -4
- package/dist/types/utils/tool-call-arguments.d.ts +8 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +130 -37
- package/src/auth/blocks.ts +94 -15
- package/src/auth/cascade.ts +112 -17
- package/src/auth/health.ts +3 -2
- package/src/auth/oauth.ts +25 -10
- package/src/auth/policy.ts +10 -1
- package/src/auth/pool.ts +0 -5
- package/src/auth/refresh.ts +17 -12
- package/src/auth/resets.ts +1 -1
- package/src/auth/rotation.ts +8 -2
- package/src/auth/select.ts +102 -30
- package/src/auth/sqlite-credential-store.ts +119 -49
- package/src/auth/store.ts +8 -2
- package/src/auth/types.ts +108 -17
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +2 -2
- package/src/auth-broker/client.ts +7 -4
- package/src/auth-broker/discover.ts +6 -5
- package/src/auth-broker/refresher.ts +8 -1
- package/src/auth-broker/remote-store.ts +200 -62
- package/src/auth-broker/server.ts +316 -130
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/dispatch.ts +15 -7
- package/src/auth-gateway/http.ts +0 -8
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +2 -2
- package/src/auth-gateway/routes/images.ts +4 -4
- package/src/auth-gateway/routes/rerank.ts +2 -2
- package/src/auth-gateway/routes/speech.ts +2 -2
- package/src/auth-gateway/routes/systemone.ts +22 -14
- package/src/auth-gateway/routes/transcriptions.ts +2 -2
- package/src/auth-gateway/routes/video.ts +7 -7
- package/src/auth-gateway/server.ts +175 -134
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/auth-gateway/stdio.ts +132 -0
- package/src/auth-storage.ts +23 -9
- package/src/dialect/deepseek.ts +67 -8
- package/src/dialect/gemini.ts +8 -2
- package/src/dialect/hermes.ts +8 -2
- package/src/dialect/kimi.ts +8 -2
- package/src/dialect/qwen-xml.ts +8 -2
- package/src/dialect/qwen3.ts +8 -2
- package/src/dialect/terminator-wait.ts +42 -0
- package/src/env-api-key.ts +78 -0
- package/src/error/auth.ts +20 -0
- package/src/error/flags.ts +143 -24
- package/src/error/rate-limit.ts +8 -6
- package/src/images/google-antigravity.ts +0 -1
- package/src/images/index.ts +3 -17
- package/src/images/openai-hosted.ts +13 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +2 -1
- package/src/judgment/index.ts +2 -1
- package/src/judgment/native.ts +333 -0
- package/src/judgment/openai-decisions-predicate.md +9 -0
- package/src/judgment/openai-decisions.ts +129 -0
- package/src/judgment/question-file.ts +90 -0
- package/src/judgment/systemone.ts +65 -0
- package/src/judgment/text.ts +9 -1
- package/src/judgment/types.ts +2 -2
- package/src/judgment/wire.ts +42 -0
- package/src/provider-details.ts +1 -1
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +19 -8
- package/src/providers/anthropic-client.ts +16 -1
- package/src/providers/anthropic-state.ts +6 -0
- package/src/providers/anthropic.ts +962 -238
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/aws-credentials.ts +65 -5
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-error-detail.ts +21 -0
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +590 -133
- package/src/providers/devin.ts +68 -58
- package/src/providers/factory-droid/gemini.ts +38 -51
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/google-gemini-cli.ts +4 -0
- package/src/providers/google-shared.ts +134 -105
- package/src/providers/google-vertex.ts +0 -1
- package/src/providers/google.ts +1 -1
- package/src/providers/inference-headers.ts +19 -0
- package/src/providers/mock.ts +4 -0
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex/request-transformer.ts +16 -11
- package/src/providers/openai-codex-responses.ts +201 -76
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-configuration-update.ts +54 -15
- package/src/providers/openai-responses-wire.ts +1 -55
- package/src/providers/openai-responses.ts +437 -33
- package/src/providers/openai-shared.ts +267 -72
- package/src/providers/pi-native-client.ts +2 -2
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +1 -1
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +240 -84
- package/src/providers/vision-guard.ts +1 -1
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +26 -3
- package/src/registry/hooks/api-key.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/callback-server.ts +6 -1
- package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
- package/src/registry/oauth/cursor.ts +80 -17
- package/src/registry/oauth/index.ts +5 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +85 -107
- package/src/transcription/types.ts +2 -1
- package/src/types.ts +142 -15
- package/src/usage/claude.ts +19 -12
- package/src/usage/commandcode.ts +1 -1
- package/src/usage/cursor.ts +14 -25
- package/src/usage/github-copilot.ts +1 -1
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +8 -1
- package/src/usage.ts +8 -0
- package/src/utils/anthropic-auth.ts +34 -10
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/block-symbols.ts +17 -0
- package/src/utils/dsml-leak.ts +109 -0
- package/src/utils/empty-completion-retry.ts +2 -3
- package/src/utils/glyph-codec.ts +200 -7
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/leaked-thinking-stream.ts +47 -25
- package/src/utils/resolve-peer.ts +7 -0
- package/src/utils/retry-wait.ts +19 -0
- package/src/utils/schema/CONSTRAINTS.md +9 -7
- package/src/utils/schema/fields.ts +20 -0
- package/src/utils/schema/normalize.ts +65 -23
- package/src/utils/schema/wire.ts +3 -3
- package/src/utils/stream-markup-healing.ts +4 -4
- package/src/utils/thinking-loop.ts +36 -13
- package/src/utils/tool-call-arguments.ts +16 -0
- package/src/utils/transport-fetch.ts +1 -1
- package/src/utils.ts +41 -10
- package/dist/types/judgment/typesafe.d.ts +0 -49
- package/src/judgment/typesafe.ts +0 -183
|
@@ -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
|
|
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
|
|
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,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
|
-
|
|
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; `
|
|
70
|
-
*
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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. */
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
156
|
-
"@linxiraos/pi-catalog": "1.
|
|
157
|
-
"@linxiraos/pi-natives": "1.
|
|
158
|
-
"@linxiraos/pi-utils": "1.
|
|
159
|
-
"@linxiraos/pi-wire": "1.
|
|
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"
|
package/src/auth/affinity.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { logger } from "@linxiraos/pi-utils";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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,
|
|
40
|
-
/** Persisted sticky rows keyed by
|
|
41
|
-
#persistedSticky: Map<string, PersistedSticky
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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,
|