@linxiraos/pi-ai 1.2.1 → 1.3.1

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 (153) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +4 -2
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/auth/cascade.d.ts +5 -3
  5. package/dist/types/auth/pool.d.ts +3 -3
  6. package/dist/types/auth/select.d.ts +5 -1
  7. package/dist/types/auth/types.d.ts +15 -5
  8. package/dist/types/auth/usage-cache.d.ts +3 -1
  9. package/dist/types/auth-gateway/clients.d.ts +67 -0
  10. package/dist/types/auth-gateway/dispatch.d.ts +28 -10
  11. package/dist/types/auth-gateway/http.d.ts +2 -0
  12. package/dist/types/auth-gateway/index.d.ts +1 -0
  13. package/dist/types/auth-gateway/server.d.ts +9 -4
  14. package/dist/types/auth-gateway/session-state.d.ts +6 -0
  15. package/dist/types/auth-gateway/types.d.ts +16 -2
  16. package/dist/types/dialect/coercion.d.ts +6 -0
  17. package/dist/types/dialect/deepseek.d.ts +13 -0
  18. package/dist/types/error/classes.d.ts +1 -1
  19. package/dist/types/error/flags.d.ts +15 -0
  20. package/dist/types/error/index.d.ts +1 -0
  21. package/dist/types/error/network-cause.d.ts +40 -0
  22. package/dist/types/images/shared.d.ts +26 -2
  23. package/dist/types/index.d.ts +1 -0
  24. package/dist/types/judgment/native.d.ts +62 -3
  25. package/dist/types/judgment/systemone.d.ts +9 -2
  26. package/dist/types/judgment/text.d.ts +11 -5
  27. package/dist/types/judgment/types.d.ts +37 -6
  28. package/dist/types/judgment/wire.d.ts +18 -3
  29. package/dist/types/provider-session-state.d.ts +2 -2
  30. package/dist/types/providers/anthropic-images.d.ts +7 -0
  31. package/dist/types/providers/anthropic-signature.d.ts +12 -2
  32. package/dist/types/providers/anthropic-state.d.ts +7 -0
  33. package/dist/types/providers/anthropic.d.ts +21 -6
  34. package/dist/types/providers/connection-prewarm.d.ts +15 -0
  35. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  36. package/dist/types/providers/cursor-pi-args.d.ts +13 -0
  37. package/dist/types/providers/cursor.d.ts +7 -0
  38. package/dist/types/providers/ollama.d.ts +6 -1
  39. package/dist/types/providers/openai-codex-responses.d.ts +6 -0
  40. package/dist/types/providers/openai-responses.d.ts +9 -0
  41. package/dist/types/providers/openai-shared.d.ts +30 -0
  42. package/dist/types/providers/systemone-server.d.ts +7 -3
  43. package/dist/types/providers/transform-messages.d.ts +28 -5
  44. package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
  45. package/dist/types/stream.d.ts +0 -2
  46. package/dist/types/types.d.ts +33 -9
  47. package/dist/types/usage/muse-code.d.ts +1 -1
  48. package/dist/types/utils/dsml-leak.d.ts +17 -13
  49. package/dist/types/utils/narration.d.ts +9 -0
  50. package/dist/types/utils/openai-http.d.ts +2 -0
  51. package/dist/types/utils/request-body.d.ts +11 -0
  52. package/dist/types/utils/request-compression.d.ts +21 -0
  53. package/dist/types/utils/tool-call-arguments.d.ts +2 -1
  54. package/dist/types/utils/transport-fetch.d.ts +8 -0
  55. package/dist/types/utils.d.ts +1 -1
  56. package/package.json +7 -7
  57. package/src/auth/cascade.ts +23 -13
  58. package/src/auth/pool.ts +3 -3
  59. package/src/auth/refresh.ts +39 -12
  60. package/src/auth/select.ts +8 -2
  61. package/src/auth/types.ts +16 -5
  62. package/src/auth/usage-cache.ts +23 -7
  63. package/src/auth/usage.ts +2 -4
  64. package/src/auth-broker/remote-store.ts +31 -6
  65. package/src/auth-gateway/clients.ts +318 -0
  66. package/src/auth-gateway/dispatch.ts +66 -35
  67. package/src/auth-gateway/http.ts +11 -5
  68. package/src/auth-gateway/index.ts +1 -0
  69. package/src/auth-gateway/routes/embeddings.ts +4 -10
  70. package/src/auth-gateway/routes/images.ts +6 -10
  71. package/src/auth-gateway/routes/rerank.ts +4 -10
  72. package/src/auth-gateway/routes/speech.ts +10 -10
  73. package/src/auth-gateway/routes/systemone.ts +22 -12
  74. package/src/auth-gateway/routes/transcriptions.ts +4 -2
  75. package/src/auth-gateway/routes/video.ts +12 -15
  76. package/src/auth-gateway/server.ts +136 -53
  77. package/src/auth-gateway/session-state.ts +14 -0
  78. package/src/auth-gateway/types.ts +16 -2
  79. package/src/dialect/coercion.ts +16 -0
  80. package/src/dialect/deepseek.ts +344 -81
  81. package/src/dialect/hermes.ts +7 -11
  82. package/src/dialect/qwen3.ts +3 -11
  83. package/src/error/classes.ts +3 -2
  84. package/src/error/flags.ts +24 -3
  85. package/src/error/index.ts +1 -0
  86. package/src/error/network-cause.ts +179 -0
  87. package/src/images/codex-format-directive.md +3 -0
  88. package/src/images/google-antigravity.ts +2 -1
  89. package/src/images/google-generative-ai.ts +4 -2
  90. package/src/images/openai-hosted.ts +25 -2
  91. package/src/images/openai-images.ts +11 -6
  92. package/src/images/shared.ts +77 -3
  93. package/src/index.ts +1 -0
  94. package/src/judgment/chat.ts +17 -1
  95. package/src/judgment/native.ts +238 -12
  96. package/src/judgment/openai-decisions.ts +30 -6
  97. package/src/judgment/systemone.ts +153 -11
  98. package/src/judgment/text.ts +26 -7
  99. package/src/judgment/types.ts +46 -6
  100. package/src/judgment/wire.ts +19 -3
  101. package/src/provider-session-state.ts +2 -2
  102. package/src/providers/amazon-bedrock.ts +4 -1
  103. package/src/providers/anthropic-client.ts +1 -1
  104. package/src/providers/anthropic-images.ts +263 -0
  105. package/src/providers/anthropic-messages-server.ts +84 -1
  106. package/src/providers/anthropic-signature.ts +35 -8
  107. package/src/providers/anthropic-state.ts +13 -0
  108. package/src/providers/anthropic.ts +479 -397
  109. package/src/providers/connection-prewarm.ts +78 -0
  110. package/src/providers/cowork-fetch.ts +5 -1
  111. package/src/providers/cursor/exec-modern.ts +1 -0
  112. package/src/providers/cursor-pi-args.ts +19 -0
  113. package/src/providers/cursor.ts +53 -13
  114. package/src/providers/devin.ts +10 -4
  115. package/src/providers/factory-droid/gemini.ts +9 -3
  116. package/src/providers/google-gemini-cli.ts +11 -1
  117. package/src/providers/google-shared.ts +2 -6
  118. package/src/providers/images-server.ts +6 -1
  119. package/src/providers/ollama.ts +23 -5
  120. package/src/providers/openai-anthropic-shim.ts +1 -0
  121. package/src/providers/openai-chat-server.ts +29 -0
  122. package/src/providers/openai-codex/access-programs.ts +2 -1
  123. package/src/providers/openai-codex-responses.ts +49 -82
  124. package/src/providers/openai-completions.ts +117 -19
  125. package/src/providers/openai-responses.ts +25 -2
  126. package/src/providers/openai-shared.ts +36 -5
  127. package/src/providers/pi-native-server.ts +1 -0
  128. package/src/providers/systemone-server.ts +72 -12
  129. package/src/providers/transform-messages.ts +104 -55
  130. package/src/registry/oauth/google-antigravity.ts +1 -1
  131. package/src/stream.ts +13 -73
  132. package/src/types.ts +33 -9
  133. package/src/usage/claude.ts +2 -9
  134. package/src/usage/github-copilot.ts +26 -4
  135. package/src/usage/muse-code.ts +115 -29
  136. package/src/usage/openai-codex.ts +13 -7
  137. package/src/usage/opencode-go.ts +7 -8
  138. package/src/usage/xai-oauth.ts +2 -9
  139. package/src/utils/dsml-leak.ts +88 -33
  140. package/src/utils/leaked-thinking-stream.ts +10 -0
  141. package/src/utils/narration.ts +18 -0
  142. package/src/utils/openai-http.ts +27 -18
  143. package/src/utils/request-body.ts +27 -0
  144. package/src/utils/request-compression.ts +202 -0
  145. package/src/utils/request-debug.ts +23 -9
  146. package/src/utils/schema/dereference.ts +51 -18
  147. package/src/utils/schema/draft.ts +10 -2
  148. package/src/utils/schema/json-schema-validator.ts +4 -10
  149. package/src/utils/schema/normalize.ts +6 -9
  150. package/src/utils/tool-call-arguments.ts +2 -1
  151. package/src/utils/transport-fetch.ts +37 -0
  152. package/src/utils/validation.ts +26 -16
  153. package/src/utils.ts +38 -23
@@ -218,6 +218,13 @@ export declare function mergeCursorMcpToolCallArgs(streamed: Record<string, unkn
218
218
  * call handed to an external executor is only a handoff while the marker is
219
219
  * absent, and the consumer that runs it needs a runnable block.
220
220
  *
221
+ * A frame whose `toolCallId` already has a block in `output` is the server
222
+ * re-dispatching that call (a new `execId`, e.g. a `read` re-sent once a
223
+ * parallel shell created the file). It adds no block: a second block with the
224
+ * same id makes every rebuilt history replay the call twice, which Cursor
225
+ * rejects (issue #15749). The re-run's result supersedes the first one in the
226
+ * host's result buffer.
227
+ *
221
228
  * Exported for tests to exercise ordering with adjacent text/thinking blocks.
222
229
  */
223
230
  export declare function synthesizeCursorExecToolCall(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, toolCallId: string, toolName: string, args: Record<string, unknown>, { executed }?: {
@@ -1,8 +1,13 @@
1
- import type { StreamFunction, StreamOptions, ToolChoice } from "../types.js";
1
+ import type { Model, StreamFunction, StreamOptions, ToolChoice } from "../types.js";
2
2
  export interface OllamaChatOptions extends StreamOptions {
3
3
  reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
4
4
  disableReasoning?: boolean;
5
5
  toolChoice?: ToolChoice;
6
6
  }
7
+ /** Whether Ollama chat removes assistant thinking from replayed history. */
8
+ export declare function ollamaChatDropsOwnThinking(model: {
9
+ api?: string;
10
+ compat?: Model["compat"];
11
+ } | null | undefined): boolean;
7
12
  /** Retry EOS-only Ollama completions before the agent loop sees an empty stop. */
8
13
  export declare const streamOllama: StreamFunction<"ollama-chat">;
@@ -6,6 +6,12 @@ export type { CodexAttestationProvider } from "./openai-codex-attestation.js";
6
6
  import type { ResponseInput } from "./openai-responses-wire.js";
7
7
  export interface OpenAICodexResponsesOptions extends StreamOptions {
8
8
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
9
+ /**
10
+ * Run with the least reasoning the model allows; takes precedence over
11
+ * `reasoning`. Sends the `none` tier where the catalog's disable mode is
12
+ * `none-effort`, else the model's lowest supported effort.
13
+ */
14
+ disableReasoning?: boolean;
9
15
  reasoningSummary?: "auto" | "concise" | "detailed" | null;
10
16
  /** Explicit `reasoning.context` replay scope. Omitted by default so Codex applies its native request policy. */
11
17
  reasoningContext?: CodexReasoningContext;
@@ -114,6 +114,15 @@ interface OpenAIResponsesChainState {
114
114
  /** Set once chaining is judged unsupported for this session (circuit breaker). */
115
115
  disabled: boolean;
116
116
  }
117
+ /**
118
+ * Whether the next request for `model` replays native Responses history items
119
+ * (encrypted reasoning, item ids), as {@link buildParams} decides it. A session
120
+ * is cold, rebuilding turns from message content, from the first request on a
121
+ * connection-bound host (`compat.connectionBoundNativeHistory`, GitHub Copilot)
122
+ * and after `close()` on any host, until a response succeeds. Without session
123
+ * state every request replays.
124
+ */
125
+ export declare function openAIResponsesReplaysNativeHistory(model: Pick<Model, "api" | "provider" | "compat"> | null | undefined, providerSessionState: Map<string, ProviderSessionState> | undefined): boolean;
117
126
  /**
118
127
  * Set the process-wide storage default per provider id, used when a request
119
128
  * leaves `storeResponses` unset. Hosts call this from their settings so every
@@ -399,6 +399,22 @@ interface StrictToolsRetryContext {
399
399
  strictToolsApplied: boolean;
400
400
  tools: Tool[] | undefined;
401
401
  }
402
+ /**
403
+ * Read OpenRouter's `error.metadata.raw` from a captured non-2xx body.
404
+ *
405
+ * OpenRouter routes to a different upstream per request, so its top-level
406
+ * `error.message` is a generic policy sentence ("Access denied by security
407
+ * policy") and the only place the routed provider's own words appear is
408
+ * `error.metadata.raw`. Providers that proxy through OpenRouter also hang the
409
+ * upstream provider name there.
410
+ *
411
+ * The detail lives in the decoded body captured on the thrown
412
+ * {@link AIError.OpenAIHttpError}, not on the error object itself: no error
413
+ * class in this package carries an `error` property, so reading
414
+ * `error.error.metadata.raw` unconditionally yielded `undefined` and the
415
+ * upstream detail was dropped from every surfaced message.
416
+ */
417
+ export declare function readOpenRouterRawMetadata(capturedErrorResponse: CapturedHttpErrorResponse | undefined): string | undefined;
402
418
  /** Decide whether an OpenAI-family request should retry once with non-strict tools. */
403
419
  export declare function shouldRetryWithoutStrictTools(error: unknown, capturedErrorResponse: CapturedHttpErrorResponse | undefined, context: StrictToolsRetryContext): boolean;
404
420
  export declare const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string>;
@@ -525,6 +541,20 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
525
541
  * Callers gate on {@link isHarmonyDialectModel}. Items are copied, not mutated.
526
542
  */
527
543
  export declare function escapeReplayedControlTokens(items: ResponseInput): ResponseInput;
544
+ /**
545
+ * Whether a same-provider assistant turn may replay its native items while the
546
+ * provider session is still cold (#489). A cold session rebuilds turns from
547
+ * message content because some backends bind native items to one connection
548
+ * (GitHub Copilot: `401 input item does not belong to this connection`, #488).
549
+ * Binding needs server-issued state that survives replay sanitization: an
550
+ * `encrypted_content` blob or an item id. A turn with neither, whose every
551
+ * reasoning item carries plaintext `reasoning_text`, has nothing to bind and is
552
+ * exactly what the server receives once the session warms; rebuilding it would
553
+ * drop that reasoning and change the prompt prefix the server cached.
554
+ * Summary-only reasoning keeps the rebuild: it is no evidence of a server that
555
+ * returns plaintext reasoning.
556
+ */
557
+ export declare function isColdReplayableResponsesTurn(items: ResponseInput): boolean;
528
558
  export declare function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput;
529
559
  /**
530
560
  * Non-empty `reasoning_text` shipped for a synthesized reasoning item when no
@@ -1,7 +1,9 @@
1
1
  import type { JudgmentRequest, JudgmentResult } from "../judgment/types.js";
2
+ import type { JudgmentImageAssets } from "../judgment/wire.js";
2
3
  export interface SystemOneParsedRequest {
3
4
  modelId: string;
4
5
  request: JudgmentRequest;
6
+ imageAssets?: JudgmentImageAssets;
5
7
  }
6
8
  /** `POST /v1/systemone` response body; `cost` (USD) appears only when the route billed one. */
7
9
  export interface SystemOneResponseBody {
@@ -14,10 +16,12 @@ export interface SystemOneResponseBody {
14
16
  };
15
17
  }
16
18
  /**
17
- * Validate the routing-relevant parts of a System One request body.
19
+ * Validate the routing-relevant parts of a System One request body, including
20
+ * top-level embedded images and inline TypeSafe image assets.
18
21
  *
19
- * @throws {AIError.ValidationError} naming the offending field when `model`,
20
- * `state`, or `questions` is missing or malformed.
22
+ * @throws {AIError.ValidationError} when required routing fields are missing
23
+ * or malformed, supplied image data is invalid, or an asset uses multipart
24
+ * `part`, which this JSON ingress cannot read.
21
25
  */
22
26
  export declare function parseRequest(body: unknown): SystemOneParsedRequest;
23
27
  /** Re-encode a judgment result in TypeSafe's response shape. */
@@ -9,6 +9,10 @@ import type { Api, AssistantMessage, Message, Model } from "../types.js";
9
9
  * MUST not push a normalized id past this bound.
10
10
  */
11
11
  export declare const MAX_TOOL_CALL_ID_LENGTH = 64;
12
+ /** Replace unsupported tool-call ID characters and cap the result for strict replay providers. */
13
+ export declare function normalizeToolCallId(id: string, maxLength?: number): string;
14
+ /** Whether `id` already satisfies the strict 1–64 `[a-zA-Z0-9_-]` tool-call ID contract. */
15
+ export declare function isValidToolCallId(id: string): boolean;
12
16
  /**
13
17
  * OpenAI Responses-family APIs mint composite tool ids (`call_id|item_id`);
14
18
  * opaque Chat Completions ids do not (openai-completions preserves same-model
@@ -25,12 +29,31 @@ export declare function isResponsesFamilyApi(api: Api | undefined): boolean;
25
29
  export declare function responsesCallComponent(id: string): string;
26
30
  export declare function appendDuplicateSuffix(originalId: string, suffix: string, maxLength: number): string;
27
31
  /**
28
- * Whether a tool-call name cannot belong to any declared tool: missing, empty,
29
- * longer than OpenAI's 128-character replay limit, or containing whitespace or
30
- * control characters (tool schemas never allow them; their presence means the
31
- * name slot carries invocation text).
32
+ * Whether a tool-call name cannot be replayed to the target: missing, empty,
33
+ * longer than its declaration limit, or containing a character outside its
34
+ * declaration charset. Models emit such names when the name slot carries
35
+ * invocation text (whitespace, NULs) or an invented internal URL (`proc://*`).
36
+ *
37
+ * Without a target, use the OpenAI Responses native-history constraints.
38
+ */
39
+ export declare function isMalformedToolCallName(name: unknown, allowedName?: RegExp, maxLength?: number): boolean;
40
+ /**
41
+ * Whether {@link transformMessages} drops `msg` from every replay: a turn cut
42
+ * off by `length` / `error` / `aborted` that carries no text and no tool call.
43
+ */
44
+ export declare function shouldDropTruncatedThinkingOnlyAssistant(msg: AssistantMessage): boolean;
45
+ /**
46
+ * Whether `msg` reaches a replay as an assistant turn, so it can be the latest
47
+ * surviving one: {@link sanitizeMalformedToolCalls} drops its malformed tool
48
+ * calls, then {@link shouldDropTruncatedThinkingOnlyAssistant} a truncated
49
+ * turn left with no text and no tool call. On an already sanitized list this
50
+ * is `!shouldDropTruncatedThinkingOnlyAssistant(msg)`. Pass the target `model`
51
+ * to apply its tool-name limits; without one, any provider-declarable name survives.
32
52
  */
33
- export declare function isMalformedToolCallName(name: unknown): boolean;
53
+ export declare function isSurvivingAssistant(msg: AssistantMessage, model?: Model): boolean;
54
+ export declare function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages">;
55
+ /** Whether `turn` came from `model` itself: its own turns replay their native reasoning unchanged. */
56
+ export declare function isSameModelTurn(turn: Pick<AssistantMessage, "provider" | "api" | "model">, model: Pick<Model, "provider" | "api" | "id">): boolean;
34
57
  /**
35
58
  * Normalize tool call ID for cross-provider compatibility.
36
59
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
2
+ * Google Antigravity OAuth flow (Gemini, Claude, GPT-OSS via Google Cloud)
3
3
  * Uses different OAuth credentials than google-gemini-cli for access to additional models.
4
4
  */
5
5
  import type { AfterExchangeHook } from "../hooks/types.js";
@@ -5,8 +5,6 @@ import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli.js";
5
5
  import type { GoogleVertexOptions } from "./providers/google-vertex.js";
6
6
  import type { Api, AssistantMessage, Context, Model, OptionsForApi, SimpleStreamOptions, ToolChoice } from "./types.js";
7
7
  import { AssistantMessageEventStream } from "./utils/event-stream.js";
8
- /** Strict official-Codex endpoint check; exact origin or a path boundary after {@link CODEX_BASE_URL}. */
9
- export declare function isOfficialCodexApiUrl(baseUrl: string | undefined): boolean;
10
8
  export declare function configureProviderMaxInFlightRequests(limits: Record<string, number> | undefined): void;
11
9
  export declare const __providerInFlightForTesting: {
12
10
  setRoot(root: string | undefined): void;
@@ -9,7 +9,7 @@ import type { ApiKey } from "./auth-retry.js";
9
9
  import type { OAuthRequestIdentity } from "./auth/types.js";
10
10
  import type { BedrockOptions } from "./providers/amazon-bedrock.js";
11
11
  import type { AnthropicOptions } from "./providers/anthropic.js";
12
- import type { FallbackParam, StopDetails } from "./providers/anthropic-wire.js";
12
+ import type { FallbackParam, MessageCreateParamsStreaming, StopDetails } from "./providers/anthropic-wire.js";
13
13
  import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
14
14
  import type { CursorOptions } from "./providers/cursor.js";
15
15
  import type { AppleFoundationModelsOptions } from "./providers/apple-foundation-models.js";
@@ -267,6 +267,11 @@ export interface StreamOptions {
267
267
  frequencyPenalty?: number;
268
268
  maxTokens?: number;
269
269
  signal?: AbortSignal;
270
+ /**
271
+ * OpenAI chat-completions HTTP attempt budget. When set, transient provider
272
+ * errors are not replayed above the transport, so retries share this limit.
273
+ */
274
+ maxHttpAttempts?: number;
270
275
  apiKey?: string;
271
276
  /** @internal Stored credential row serving this request, when known. */
272
277
  credentialId?: number;
@@ -398,8 +403,6 @@ export interface StreamOptions {
398
403
  liveSteering?: LiveSteering;
399
404
  /** Canonical Codex compaction classification; ignored by other providers. */
400
405
  codexCompaction?: CodexCompactionRequestContext;
401
- /** Codex Code Mode tool exposure snapshot emitted as `tool_namespaces_info` turn metadata; ignored by other providers. */
402
- toolNamespacesInfo?: unknown;
403
406
  /**
404
407
  * Optional per-provider concurrent request cap for LLM stream calls. Keys are
405
408
  * provider ids (`model.provider`); positive numeric values cap in-flight
@@ -645,6 +648,13 @@ export interface ThinkingContent {
645
648
  * replay natively on another host.
646
649
  */
647
650
  summary?: boolean;
651
+ /**
652
+ * `true`: the block is narration — prose the model addressed to the user
653
+ * between tool calls, which the provider returned as a signed thinking block
654
+ * (Anthropic connector-text summarization). It stays a thinking block for
655
+ * replay; user-facing surfaces present it as answer text.
656
+ */
657
+ narration?: boolean;
648
658
  }
649
659
  export interface RedactedThinkingContent {
650
660
  type: "redactedThinking";
@@ -652,11 +662,12 @@ export interface RedactedThinkingContent {
652
662
  }
653
663
  /**
654
664
  * Anthropic server-side-fallback boundary marker persisted on assistant
655
- * turns whose provider request opted into
656
- * `AnthropicOptions.fallbacks`. Consumers other than the Anthropic
657
- * provider MUST ignore it — `transformMessages` strips the block on any
658
- * cross-provider hop and on non-official Anthropic replays, so downstream
659
- * converters never see it.
665
+ * turns whose response switched models mid-stream, whether the request
666
+ * opted into `AnthropicOptions.fallbacks` or a gateway opted in on its
667
+ * behalf. Consumers other than the Anthropic provider MUST ignore it —
668
+ * `transformMessages` strips the block on any non-Anthropic hop, and
669
+ * `convertAnthropicMessages` only puts it on the wire for an opted-in
670
+ * request to the official endpoint.
660
671
  */
661
672
  export interface AnthropicFallbackContent {
662
673
  type: "fallback";
@@ -983,6 +994,12 @@ export type AssistantRetryRecovery = {
983
994
  export interface ContextSnapshot {
984
995
  promptTokens: number;
985
996
  nonMessageTokens: number;
997
+ /**
998
+ * Estimated tool-schema tokens included in `nonMessageTokens` (0 when the
999
+ * model defers schemas server-side). Lets a later model whose schema policy
1000
+ * differs rebase the provider anchor instead of inheriting its schemas.
1001
+ */
1002
+ toolSchemaTokens?: number;
986
1003
  /** Estimated prompt tokens removed by local history rewrites after this provider snapshot was recorded. */
987
1004
  historyRewriteTokensRemoved?: number;
988
1005
  /**
@@ -1031,6 +1048,12 @@ export interface AssistantMessage {
1031
1048
  */
1032
1049
  serviceTier?: ServiceTier;
1033
1050
  usage: Usage;
1051
+ /**
1052
+ * Visible output tokens a history rewrite (shake) has since removed from
1053
+ * `content`. `usage.output` still counts them, so a replay estimate subtracts
1054
+ * them to keep the turn's reasoning without recharging the removed text.
1055
+ */
1056
+ elidedOutputTokens?: number;
1034
1057
  stopReason: StopReason;
1035
1058
  stopDetails?: StopDetails | null;
1036
1059
  errorMessage?: string;
@@ -1418,7 +1441,8 @@ export type AssistantMessageEvent = {
1418
1441
  export interface AnthropicFallbackCreditHandle {
1419
1442
  token: string;
1420
1443
  prefillClaim?: boolean | null;
1421
- params: unknown;
1444
+ /** The refused request's body, replayed with the credit token on the fallback model. */
1445
+ params: MessageCreateParamsStreaming;
1422
1446
  betas?: readonly string[];
1423
1447
  betaHeader?: string;
1424
1448
  expiresAt: number;
@@ -1,2 +1,2 @@
1
- import type { UsageProvider } from "../usage.js";
1
+ import { type UsageProvider } from "../usage.js";
2
2
  export declare const museCodeUsageProvider: UsageProvider;
@@ -1,29 +1,33 @@
1
1
  /**
2
- * Removal of DeepSeek DSML tool-call markup left in visible assistant text.
2
+ * Removal of DeepSeek tool-call markup left in visible assistant text.
3
3
  *
4
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.
5
+ * text in `content`: the V4 `<|DSML|…>` envelope or the V3 chat-template
6
+ * tokens (`<|tool▁calls▁begin|>` …). A well-formed call is healed into a
7
+ * structured call by `StreamMarkupHealing`, but a degraded model can drop
8
+ * required tags (or the host runs no healer at all). That markup must not stay
9
+ * in history: replaying it teaches the model to keep writing tool calls as text
10
+ * (issue #10556). Callers strip it before the message is committed and tell the
11
+ * model its call failed.
11
12
  */
12
13
  import type { AssistantMessage, Model } from "../types.js";
13
14
  /**
14
15
  * Whether DSML leak recovery applies to `model`: a DeepSeek-class model, or one
15
16
  * 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.
17
+ * whose id the classifier cannot place). Other models never write DeepSeek tool
18
+ * calls, so such markup from them is prose and is left alone.
18
19
  */
19
20
  export declare function isDsmlLeakRecoveryTarget(model: Model): boolean;
20
21
  /**
21
- * Strip leaked DSML tool-call markup from `text`, leaving quoted code intact.
22
- * Returns `undefined` when `text` carries no such markup.
22
+ * Strip leaked DeepSeek tool-call markup (DSML or chat-template tokens) from
23
+ * `text`, leaving quoted code intact. Returns `undefined` when `text` carries
24
+ * no such markup.
23
25
  */
24
26
  export declare function stripDsmlToolMarkup(text: string): string | undefined;
25
27
  /**
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
+ * Remove leaked DSML tool-call markup from an assistant message before commit,
29
+ * including reasoning blocks and turns with valid structured calls. Rewritten
30
+ * thinking with an opaque signature is discarded; OpenAI-compatible reasoning
31
+ * field names are replay markers, not signatures, and survive cleanup.
28
32
  */
29
33
  export declare function removeDsmlToolMarkupLeak(message: AssistantMessage): boolean;
@@ -0,0 +1,9 @@
1
+ import type { AssistantMessage } from "../types.js";
2
+ /**
3
+ * The message as a user-facing surface should present it: every narration
4
+ * thinking block (see `ThinkingContent.narration`) becomes a text block, so it
5
+ * renders as part of the answer instead of folding into reasoning. Returns the
6
+ * input unchanged when it holds no narration. Display-only: replay must keep
7
+ * the original thinking blocks and their signatures.
8
+ */
9
+ export declare function presentNarrationAsAnswer(message: AssistantMessage): AssistantMessage;
@@ -26,6 +26,8 @@ export interface OpenAIStreamRequestInit {
26
26
  body: unknown;
27
27
  signal: AbortSignal;
28
28
  fetch?: FetchImpl;
29
+ /** Override the default transport attempt budget for this request. */
30
+ maxAttempts?: number;
29
31
  /** Optional caller-specific gate composed with shared transport retry exclusions. */
30
32
  shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
31
33
  /**
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bytes of a request body as the server reads them: the `gzip`/`zstd`
3
+ * `Content-Encoding` applied by request compression is undone, any other
4
+ * encoding is returned untouched.
5
+ */
6
+ export declare function decodeRequestBodyBytes(bytes: Uint8Array, contentEncoding: string | null): Uint8Array;
7
+ /**
8
+ * Text of a string or byte request body after undoing its content encoding;
9
+ * `undefined` when the request has no such body (none, stream, form data).
10
+ */
11
+ export declare function requestBodyText(init: RequestInit | undefined): string | undefined;
@@ -0,0 +1,21 @@
1
+ import type { FetchImpl, Model } from "../types.js";
2
+ /**
3
+ * Compresses request bodies from {@link model} to its provider's own endpoint
4
+ * when its catalog rules declare encodings the endpoint decodes
5
+ * (`request-encodings` / `endpoint-hosts` KDL axes). Long sessions resend
6
+ * multi-MB transcripts every turn, so upload time dominates time-to-first-token
7
+ * on ordinary uplinks.
8
+ *
9
+ * Install it beneath any body rewriter or signer (e.g. the cch attestation
10
+ * patch): those must see the uncompressed bytes. Other hosts, small bodies,
11
+ * bodies that already carry `Content-Encoding`, and `PI_REQUEST_COMPRESSION=0`
12
+ * pass through untouched. A possible rejection — the host's decode error, or a
13
+ * non-JSON 400/403/415 from a proxy in front of it — is resent uncompressed
14
+ * once; only when that resend succeeds is the encoding latched off for the
15
+ * host, so a genuine 400 never disables compression. When a non-JSON refusal
16
+ * recurs uncompressed with the same status and a non-JSON body, that status
17
+ * stops being re-checked for the host.
18
+ */
19
+ export declare function wrapFetchForRequestCompression(base: FetchImpl, model: Model): FetchImpl;
20
+ /** Test hook: clears every per-host latch. */
21
+ export declare function resetRequestCompressionForTesting(): void;
@@ -5,7 +5,8 @@ export declare function parseToolCallArguments(json: string | undefined): ToolCa
5
5
  * Native Responses `function_call.arguments` to persist for replay.
6
6
  *
7
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).
8
+ * repaired value; replay must carry what was executed (#14155), not the {@link invalidToolCallArguments}
9
+ * diagnostic it substitutes for stored arguments that do not parse (#15568).
9
10
  * Returns `raw` when it already parses or the arguments were unrepairable, else the executed arguments.
10
11
  */
11
12
  export declare function replayableToolCallArguments(raw: string, executed: ToolCall["arguments"]): string;
@@ -12,6 +12,14 @@ import type { Api, FetchImpl, Model } from "../types.js";
12
12
  * layer (three PI_REQ_DEBUG dumps for one request).
13
13
  */
14
14
  export declare function transportFetch(model: Model<Api>, fetchImpl: FetchImpl | undefined): FetchImpl;
15
+ /**
16
+ * Origin of the last inference request {@link model} actually sent, after
17
+ * every dispatch-time rewrite (credential endpoints, env overrides, regional
18
+ * hosts); `undefined` before its first request.
19
+ */
20
+ export declare function lastDispatchOrigin(model: Model<Api>): string | undefined;
21
+ /** Test hook: forgets every recorded dispatch origin. */
22
+ export declare function resetDispatchOriginsForTesting(): void;
15
23
  /** Options-bag form of {@link transportFetch}; returns `options` untouched when its fetch is already built. */
16
24
  export declare function withTransportFetch<T extends {
17
25
  fetch?: FetchImpl;
@@ -1,4 +1,5 @@
1
1
  import type { ResponseInput } from "./providers/openai-responses-wire.js";
2
+ export { normalizeToolCallId } from "./providers/transform-messages.js";
2
3
  import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } from "./types.js";
3
4
  export { isRecord } from "@linxiraos/pi-utils";
4
5
  /**
@@ -9,7 +10,6 @@ export { isRecord } from "@linxiraos/pi-utils";
9
10
  */
10
11
  export declare function getHeaderCaseInsensitive(headers: Record<string, string> | undefined, headerName: string): string | undefined;
11
12
  export declare function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[];
12
- export declare function normalizeToolCallId(id: string): string;
13
13
  type ResponsesToolItemIdPrefix = "fc" | "ctc";
14
14
  /** Preserve opaque call IDs for Responses replay while normalizing or generating the separate item ID. */
15
15
  export declare function normalizeResponsesToolCallId(id: string, itemPrefix?: ResponsesToolItemIdPrefix): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-ai",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -145,18 +145,18 @@
145
145
  },
146
146
  "scripts": {
147
147
  "check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
148
- "check:types": "tsgo -p tsconfig.json --noEmit",
148
+ "check:types": "tsc -p tsconfig.json --noEmit",
149
149
  "lint": "oxlint .",
150
150
  "test": "bun test --parallel",
151
151
  "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
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.2.1",
156
- "@linxiraos/pi-catalog": "1.2.1",
157
- "@linxiraos/pi-natives": "1.2.1",
158
- "@linxiraos/pi-utils": "1.2.1",
159
- "@linxiraos/pi-wire": "1.2.1"
155
+ "@linxiraos/pi-omptype": "1.3.1",
156
+ "@linxiraos/pi-catalog": "1.3.1",
157
+ "@linxiraos/pi-natives": "1.3.1",
158
+ "@linxiraos/pi-utils": "1.3.1",
159
+ "@linxiraos/pi-wire": "1.3.1"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@types/bun": "1.4.3"
@@ -10,6 +10,7 @@ import type { CredentialPool } from "./pool";
10
10
  import type { CredentialSelector, OAuthResolutionResult } from "./select";
11
11
  import type {
12
12
  AuthApiKeyOptions,
13
+ AuthPeekOptions,
13
14
  AuthCredential,
14
15
  AuthSource,
15
16
  AuthSourceOptions,
@@ -275,9 +276,11 @@ export class KeyCascade implements KeysApi {
275
276
  * Used for model discovery where we only need to know if credentials exist
276
277
  * and get a best-effort token. GitHub Copilot's peek must preserve
277
278
  * enterprise routing metadata because discovery needs a structured
278
- * credential to reach the correct host.
279
+ * credential to reach the correct host. With several stored accounts every
280
+ * peek returns the same one, so credential-scoped discovery caches stay on
281
+ * one namespace.
279
282
  */
280
- async peek(provider: string): Promise<string | undefined> {
283
+ async peek(provider: string, options?: AuthPeekOptions): Promise<string | undefined> {
281
284
  const runtimeKey = this.#deps.overrides.runtimeKey(provider);
282
285
  if (runtimeKey) {
283
286
  return runtimeKey;
@@ -292,19 +295,26 @@ export class KeyCascade implements KeysApi {
292
295
 
293
296
  // Precedence: a deliberate OAuth/login credential wins, then an explicit env var,
294
297
  // then a stored static api_key (which may be a stale broker-migrated copy) as a last resort.
295
- const oauthSelection = this.#deps.selector.selectByType(provider, "oauth");
298
+ // Peek never refreshes, so only an unexpired OAuth access token is usable
299
+ // unless the caller only needs the account it names.
300
+ const now = Date.now();
301
+ const oauthSelection =
302
+ this.#deps.selector.selectByType(
303
+ provider,
304
+ "oauth",
305
+ undefined,
306
+ credential =>
307
+ credential.type === "oauth" && Number.isFinite(credential.expires) && credential.expires > now,
308
+ ) ?? (options?.includeExpired ? this.#deps.selector.selectByType(provider, "oauth") : undefined);
296
309
  if (oauthSelection) {
297
- const expiresAt = oauthSelection.credential.expires;
298
- if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
299
- if (provider === "github-copilot") {
300
- return JSON.stringify({
301
- token: oauthSelection.credential.access,
302
- enterpriseUrl: oauthSelection.credential.enterpriseUrl,
303
- apiEndpoint: oauthSelection.credential.apiEndpoint,
304
- });
305
- }
306
- return oauthSelection.credential.access;
310
+ if (provider === "github-copilot") {
311
+ return JSON.stringify({
312
+ token: oauthSelection.credential.access,
313
+ enterpriseUrl: oauthSelection.credential.enterpriseUrl,
314
+ apiEndpoint: oauthSelection.credential.apiEndpoint,
315
+ });
307
316
  }
317
+ return oauthSelection.credential.access;
308
318
  }
309
319
 
310
320
  const loginApiKeySelection = this.#deps.selector.selectByType(
package/src/auth/pool.ts CHANGED
@@ -182,7 +182,7 @@ export class CredentialPool implements CredentialsApi {
182
182
  * The store is shared across every zeta process, but the pool is an
183
183
  * in-process cache refreshed only by this process's own writes. Without
184
184
  * this a long-running session ranks a stale pool for its whole lifetime:
185
- * `zeta-c auth` in another terminal is invisible, rotation reports no usable
185
+ * `zetacode auth` in another terminal is invisible, rotation reports no usable
186
186
  * sibling while a freshly added account sits unblocked in SQLite, and the
187
187
  * turn degrades to the fallback chain. The auth-broker path already polls;
188
188
  * direct-store sessions had no equivalent.
@@ -719,7 +719,7 @@ export class CredentialPool implements CredentialsApi {
719
719
  }
720
720
 
721
721
  /**
722
- * Disabled credential tombstones for display surfaces (`zeta-c usage`,
722
+ * Disabled credential tombstones for display surfaces (`zetacode usage`,
723
723
  * broker `GET /v1/credentials/disabled`). Empty when the backing store
724
724
  * keeps no tombstones or the remote broker predates the endpoint.
725
725
  */
@@ -732,7 +732,7 @@ export class CredentialPool implements CredentialsApi {
732
732
  * Force the backing store to revalidate its credential snapshot, then
733
733
  * reload. Remote broker stores re-fetch the snapshot; local stores are
734
734
  * always current, so only the reload runs. Callers that pair live
735
- * per-credential data with stored identities (`zeta-c usage`) use this so a
735
+ * per-credential data with stored identities (`zetacode usage`) use this so a
736
736
  * disk-cached snapshot cannot misattribute fresh reports.
737
737
  */
738
738
  async revalidate(): Promise<void> {