@oh-my-pi/pi-ai 18.6.0 → 18.6.3

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 (42) hide show
  1. package/CHANGELOG.md +32 -38
  2. package/README.md +15 -0
  3. package/dist/types/auth/types.d.ts +4 -0
  4. package/dist/types/auth-broker/refresher.d.ts +6 -1
  5. package/dist/types/error/flags.d.ts +15 -0
  6. package/dist/types/providers/anthropic.d.ts +15 -2
  7. package/dist/types/providers/openai-responses.d.ts +32 -3
  8. package/dist/types/registry/oauth/snowflake.d.ts +3 -0
  9. package/dist/types/registry/snowflake.d.ts +4 -0
  10. package/dist/types/types.d.ts +40 -3
  11. package/dist/types/utils/idle-iterator.d.ts +11 -0
  12. package/dist/types/utils/stream-markup-healing.d.ts +4 -4
  13. package/dist/types/utils.d.ts +6 -0
  14. package/package.json +6 -6
  15. package/src/auth/cascade.ts +6 -2
  16. package/src/auth/health.ts +1 -0
  17. package/src/auth/select.ts +3 -1
  18. package/src/auth/types.ts +4 -0
  19. package/src/auth-broker/refresher.ts +8 -1
  20. package/src/dialect/deepseek.ts +24 -3
  21. package/src/error/flags.ts +22 -0
  22. package/src/providers/anthropic.ts +111 -45
  23. package/src/providers/devin.ts +3 -1
  24. package/src/providers/factory-droid.ts +2 -0
  25. package/src/providers/gitlab-duo.ts +1 -0
  26. package/src/providers/openai-anthropic-shim.ts +1 -0
  27. package/src/providers/openai-codex-responses.ts +14 -11
  28. package/src/providers/openai-completions.ts +3 -0
  29. package/src/providers/openai-responses.ts +349 -6
  30. package/src/providers/openai-shared.ts +39 -16
  31. package/src/providers/pi-native-server.ts +1 -0
  32. package/src/registry/hooks/custom.ts +2 -0
  33. package/src/registry/oauth/snowflake.ts +183 -0
  34. package/src/registry/registry.ts +2 -0
  35. package/src/registry/snowflake.ts +149 -0
  36. package/src/stream.ts +35 -33
  37. package/src/types.ts +41 -3
  38. package/src/usage/claude.ts +3 -0
  39. package/src/utils/idle-iterator.ts +80 -35
  40. package/src/utils/stream-markup-healing.ts +4 -4
  41. package/src/utils/thinking-loop.ts +18 -10
  42. package/src/utils.ts +19 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.6.3] - 2026-10-06
6
+
7
+ ### Added
8
+
9
+ - Added per-model `compat.statefulResponses` to enable or disable stored Responses chaining (`previous_response_id` with `store: true`) for one endpoint without the official-only request fields that `compat.officialEndpoint` implies ([#13686](https://github.com/can1357/oh-my-pi/pull/13686) by [@alphastorm](https://github.com/alphastorm)).
10
+ - Added Snowflake Cortex with browser OAuth, token refresh, PAT environment authentication, and streaming Claude/OpenAI models with local tool execution ([#14507](https://github.com/can1357/oh-my-pi/pull/14507) by [@jorgoose](https://github.com/jorgoose)).
11
+ - `AuthStorage.health.check()` accepts `excludeProviders` to skip credentials of providers the caller does not serve ([#14234](https://github.com/can1357/oh-my-pi/pull/14234) by [@will-bogusz](https://github.com/will-bogusz))
12
+
13
+ ### Changed
14
+
15
+ - Reduced CPU spent on thinking-loop detection while streaming long reasoning ([#14284](https://github.com/can1357/oh-my-pi/pull/14284) by [@abilliontokens](https://github.com/abilliontokens)).
16
+ - Added `recordAffinity: false` to credential resolution options, selecting as the session would without pinning the choice to that session ([#14512](https://github.com/can1357/oh-my-pi/pull/14512) by [@will-bogusz](https://github.com/will-bogusz))
17
+
18
+ ### Fixed
19
+
20
+ - Codex native-lane steering rejections (`unsupported_native_inflight_message`) now classify as retryable from their error text alone, matching the provider's own classification, and `AIError.isCodexSteerRejection()` identifies them so the agent retry can stay on the same model ([#14242](https://github.com/can1357/oh-my-pi/pull/14242) by [@alphastorm](https://github.com/alphastorm))
21
+ - Fixed replayed Responses and Codex history, including persisted Codex user/developer and assistant items, sending `detail: "original"` images to endpoints whose `supportsImageDetailOriginal` is off ([#13687](https://github.com/can1357/oh-my-pi/pull/13687) by [@alphastorm](https://github.com/alphastorm)).
22
+ - Fixed thinking in turns kept after Anthropic native compaction being rejected or dropped on the next request ([#14251](https://github.com/can1357/oh-my-pi/pull/14251) by [@will-bogusz](https://github.com/will-bogusz))
23
+ - Fixed Claude usage being re-polled every 10 seconds while Anthropic rate-limits the account; a failed refresh now waits a minute before trying again ([#14515](https://github.com/can1357/oh-my-pi/pull/14515) by [@will-bogusz](https://github.com/will-bogusz))
24
+ - Fixed Devin requests skipping the `onPayload` hook, so payload capture now sees each Devin chat request and a returned replacement is what gets sent ([#14506](https://github.com/can1357/oh-my-pi/pull/14506) by [@will-bogusz](https://github.com/will-bogusz))
25
+ - An OpenAI Responses turn whose connection drops mid-stream can now recover the finished answer from the provider (on hosts that store results, such as Muse Code) instead of re-running the whole turn and discarding the reasoning already done. Storage is opt-in: pass `storeResponses: true`, set `PI_MUSE_STORE_RESPONSES=1`, or set a process-wide default with `configureProviderStoreResponses` ([#14293](https://github.com/can1357/oh-my-pi/pull/14293) and [#14534](https://github.com/can1357/oh-my-pi/pull/14534) by [@abilliontokens](https://github.com/abilliontokens)).
26
+ - Fixed the auth broker exiting when a background OAuth refresh sweep cannot read the credential store; the failure is now logged and the next sweep retries ([#14538](https://github.com/can1357/oh-my-pi/issues/14538))
27
+ - Fixed Anthropic OAuth billing headers changing during developer-first sessions and side turns, preserving the prompt-cache prefix ([#14495](https://github.com/can1357/oh-my-pi/issues/14495)).
28
+ - Fixed OpenAI-compatible chat-completions gateways recording completed turns as client-cancelled because the connection closed before their `[DONE]` sentinel arrived ([#14481](https://github.com/can1357/oh-my-pi/issues/14481)).
29
+
30
+ ## [18.6.1] - 2026-10-04
31
+
32
+ ### Fixed
33
+
34
+ - Fixed compatibility with Command Code DeepSeek and other DeepSeek-family models by preserving the reasoning context required for warm OpenAI Responses sessions and correctly handling incomplete DSML tool-call wrappers in visible output.
35
+
5
36
  ## [18.6.0] - 2026-10-03
6
37
 
7
38
  ### Fixed
@@ -2284,41 +2315,4 @@
2284
2315
  - Fixed truncated Gemini and Gemma tool blocks from being emitted as plain text during streaming
2285
2316
  - Fixed Gemini/Gemma in-band tool-call parsing around Python comments, raw/unicode string literals, and Gemma close-token text inside string values.
2286
2317
 
2287
- ## [15.13.2] - 2026-06-15
2288
-
2289
- ### Added
2290
-
2291
- - Added `jsonSchemaToTypeScript` to `@oh-my-pi/pi-ai/utils/schema` to render JSON Schema argument shapes as compact, human-readable TypeScript-style signatures
2292
- - Added the generic `ToolExample` type (`ToolCallExample`/`ToolCompareExample`/`ToolNoteExample`, parameterized over a tool's argument shape) and an `examples` property on the `Tool` interface for defining tool-call examples once as data.
2293
- - Added `renderToolExamples` (via `@oh-my-pi/pi-ai/grammar`) to render a tool's examples into an `<examples>` block in the model's native tool-call syntax, with an optional `_i` intent-field placeholder injected when intent tracing is active.
2294
- - Added per-grammar `renderToolCall` rendering of a single tool-call invocation (the inner element only, without the parallel-call block envelope), distinct from `renderAssistantToolCalls` which renders a complete block of one or more parallel calls.
2295
- - Added a `GrammarRenderOptions.example` flag to `renderToolCall`: when set, the invocation renders as the bare payload — Harmony emits just the JSON arguments, dropping the verbose `<|start|>…<|message|>…<|call|>` envelope — so `renderToolExamples` keeps `<examples>` blocks legible.
2296
- - Added an `abortOnFabrication` parameter to `wrapInbandToolStream` (default `true`): when `false`, a fabricated in-band tool-result continuation is discarded without aborting the provider request instead of cutting the turn short.
2297
- - Added `@oh-my-pi/pi-ai/utils/harmony-leak` export with helpers to detect, audit, and recover GPT-5 Harmony tool-call header leaks
2298
- - Added the `@oh-my-pi/pi-ai/grammar` public entrypoint for grammar factories, prompt/call rendering, in-band scanning, history encoding, and related typed utilities
2299
- - Added a unified in-band tool-call grammar engine with syntax-owned scanners, prompts, history rendering, tool-result rendering, and stream adaptation for GLM, Hermes/Qwen, Kimi, XML/Anthropic, DeepSeek, Harmony, and pi-native formats.
2300
-
2301
- ### Changed
2302
-
2303
- - Changed Harmony in-band tool-call rendering to omit the `<|constrain|>json` marker before the payload in `commentary` channel calls
2304
- - Changed tool inventory rendering to present each tool’s `Parameters` section as a simplified TypeScript-style signature derived from its wire schema
2305
- - Added raw in-band tool-call block capture to parsed owned tool calls so debugging can inspect the exact model-emitted call syntax.
2306
- - Moved the canonical `ToolCallSyntax` union to `@oh-my-pi/pi-catalog/identity` and re-exported it from `@oh-my-pi/pi-ai/grammar` so the catalog can own the syntax vocabulary without an `@oh-my-pi/pi-ai` runtime import; all existing import paths are unchanged.
2307
- - Made tool-call argument validation more lenient for schema-directed scalar coercions, including object/array stringification and 0/1 boolean coercion.
2308
- - Changed `renderToolInventory` (the verbose system-prompt inventory and `/dump`) to render each tool as a `# Tool: <name>` markdown section instead of a `<tool name="…">…</tool>` wrapper.
2309
-
2310
- ### Fixed
2311
-
2312
- - Fixed Harmony leak handling support by adding `recoverHarmonyToolCall` plus leak-detection workflows for contaminated assistant messages so recoverable tool-call arguments can be safely truncated and retried
2313
- - Fixed false-positive gating in Harmony leak heuristics using signal-based checks so unrelated text containing `to=functions...` is not treated as leaked tool-call markup
2314
- - Routed Kimi, DeepSeek DSML, and plain thinking markup healing through the shared in-band scanners so provider leak repair and owned tool calling parse the same wire formats.
2315
- - Fixed Cursor provider (`cursor-agent` API) streaming dropping large MCP tool-call arguments — most visibly the built-in `task` tool's `tasks` array on multi-subagent dispatches, which failed downstream schema validation with `tasks: Invalid input: expected array, received undefined`. Two upstream behaviors were fighting the stream handler in `packages/ai/src/providers/cursor.ts`: (1) `args_text_delta` carries the _cumulative_ args text so far per `agent.proto`, but the handler concatenated each snapshot onto the buffer, garbling the JSON; (2) `tool_call_completed` carries an `McpArgs` map that omits oversized parameters entirely and downgrades unparsable values to their raw string fallback, but the handler unconditionally overwrote the streamed args with that map. The handler now strips the already-buffered prefix from each `args_text_delta` snapshot (falling back to append when the snapshot doesn't extend the buffer) and merges the decoded `McpArgs` map into the streamed args — preserving streamed keys the completion frame omits and the structured value when the completion frame downgrades to a string. ([#2615](https://github.com/can1357/oh-my-pi/issues/2615))
2316
- - Fixed Codex Responses stream mis-routing interleaved `function_call_arguments.delta` events when more than one tool call was open concurrently. The runtime tracked a singleton `currentItem`/`currentBlock`, so every delta — regardless of `item_id` — was appended to whichever item was most recently added, and `output_item.done` for the earlier call then overwrote a sibling's stored arguments (visible as `tasks: Invalid input: expected array, received undefined` on the `task` tool). Open items are now keyed by `item_id` with `output_index` fallback; deltas/done events route to the matching block, late deltas whose item already closed are dropped instead of corrupting a sibling, and `toolcall_*` stream events emit the right `contentIndex` per call ([#2619](https://github.com/can1357/oh-my-pi/issues/2619)).
2317
-
2318
- ## [15.13.1] - 2026-06-15
2319
-
2320
- ### Fixed
2321
-
2322
- - Fixed the auth-broker (`OMP_AUTH_BROKER_URL`) rejecting OAuth credentials that carry provider-specific extension fields (e.g. an MCP server's `tokenUrl`/`clientId`/`clientSecret`/`resource` embedded for self-contained token refresh): the OAuth credential wire schema was `.strict()`, so `POST /v1/credential` failed with `400 unrecognized_keys` and a broker-backed MCP reauth reported success while the reloaded credential lacked its refresh material and could no longer refresh. The OAuth wire schema now uses `.loose()` to preserve unknown fields — matching the field-preserving local SQLite store — so extra OAuth fields round-trip through broker set->get (envelope and API-key schemas stay strict).
2323
-
2324
- Older entries are archived in [packages/ai/CHANGELOG.md@bac7e83b5b0e](https://github.com/can1357/oh-my-pi/blob/bac7e83b5b0eb86c909c17830a6666efc359578b/packages/ai/CHANGELOG.md).
2318
+ Older entries are archived in [packages/ai/CHANGELOG.md@a20cc0d04b64](https://github.com/can1357/oh-my-pi/blob/a20cc0d04b64f4a68fcc559cbd258743d984c50e/packages/ai/CHANGELOG.md).
package/README.md CHANGED
@@ -77,6 +77,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
77
77
  - **QwenCloud Token Plan** (supports `/login alibaba-token-plan`, `ALIBABA_TOKEN_PLAN_API_KEY`, or `BAILIAN_TOKEN_PLAN_API_KEY`; interactive login first selects a region — International (Singapore, default), China (Beijing) for 百炼 Token Plan keys, or a custom base URL — since region keys are non-interchangeable, then optionally stores a `home.qwencloud.com` Cookie request header for best-effort 5-hour and 7-day quota reporting)
78
78
  To enable quota reporting, sign in to the Token Plan dashboard, copy the `Cookie` request-header value from a `home.qwencloud.com` request in browser developer tools, and paste it at the second login prompt. Press Enter to skip; the Cookie is sensitive and session-lived, so rerun login when it expires.
79
79
  - **Cloudflare AI Gateway** (supports `/login cloudflare-ai-gateway`, or `CLOUDFLARE_AI_GATEWAY_API_KEY` with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_GATEWAY_ID`)
80
+ - **Snowflake Cortex** (browser `/login snowflake`, or `SNOWFLAKE_ACCOUNT` + `SNOWFLAKE_PAT`; account-billed Cortex REST API)
80
81
  - **Ollama** (local OpenAI-compatible runtime; optional `OLLAMA_API_KEY`)
81
82
  - **Ollama Cloud** (hosted native Ollama API; requires `OLLAMA_CLOUD_API_KEY`)
82
83
  - **llama.cpp** (local OpenAI and Anthropic compatible inference server)
@@ -961,6 +962,7 @@ In Node.js environments, you can set environment variables to avoid passing API
961
962
  | ZenMux | `ZENMUX_API_KEY` |
962
963
  | vLLM | `VLLM_API_KEY` |
963
964
  | Cloudflare AI Gateway | `CLOUDFLARE_AI_GATEWAY_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_GATEWAY_ID` |
965
+ | Snowflake Cortex | `SNOWFLAKE_PAT` + `SNOWFLAKE_ACCOUNT` |
964
966
  | GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |
965
967
 
966
968
  `/login cloudflare-ai-gateway` collects and stores the gateway token, account ID, and gateway ID. For environment configuration, set all three Cloudflare values above. OMP derives provider endpoints from the account and gateway IDs.
@@ -1029,9 +1031,22 @@ Several providers support OAuth authentication (some also support static API key
1029
1031
  - **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)
1030
1032
  - **Antigravity** (Free Gemini 3, Claude, GPT-OSS via Google Cloud)
1031
1033
  - **Qwen Portal** (Qwen OAuth token or API key)
1034
+ - **Snowflake Cortex** (Snowflake account OAuth with PKCE and refresh, subject to the account's local-application integration)
1032
1035
 
1033
1036
  For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.
1034
1037
 
1038
+ ### Snowflake Cortex
1039
+
1040
+ `/login snowflake` prompts for an account identifier (for example, `myorg-myaccount`), an HTTPS Snowflake account URL, or a Snowsight account link. It opens Snowflake's built-in `SNOWFLAKE$LOCAL_APPLICATION` OAuth flow using PKCE and a `127.0.0.1` callback (port 54551, with an ephemeral-port fallback). The stored account selects the inference endpoint; OAuth credentials cannot be sent to a different account through a custom model endpoint.
1041
+
1042
+ The default inference role must hold either `SNOWFLAKE.CORTEX_USER` or the narrower, REST-only `SNOWFLAKE.CORTEX_REST_API_USER` (recommended for least privilege). `CORTEX_REST_API_USER` cannot be granted to a user directly; grant it to a custom role, and revoke `CORTEX_USER` from users who already hold it so the narrower control applies. The Cortex REST API is not available in China-region (`.snowflakecomputing.cn`) accounts. An administrator can check local-app availability with `SHOW SECURITY INTEGRATIONS LIKE 'SNOWFLAKE$LOCAL_APPLICATION'` and enable the integration or refresh issuance if necessary. When Snowflake issues a refresh token, omp refreshes the access token automatically. Without one, login remains valid until expiry; then sign in again or use the PAT environment alternative.
1043
+
1044
+ For PAT authentication, set both `SNOWFLAKE_ACCOUNT` and `SNOWFLAKE_PAT`. Prefer a short-lived PAT restricted to a dedicated inference role and the required user-scoped network allowlist; do not disable network-policy enforcement. See [Snowflake PAT setup](https://docs.snowflake.com/en/user-guide/programmatic-access-tokens) and [local-application OAuth](https://docs.snowflake.com/en/user-guide/oauth-local-applications).
1045
+
1046
+ Claude models use `/api/v2/cortex/v1/messages`; OpenAI models use `/api/v2/cortex/v1/chat/completions`. Both reuse omp's streaming transports and local tool execution. The 15 bundled models are a documented roster, not an account entitlement check: availability still depends on Cortex REST API enablement, role grants, model allowlists, and region. If authentication succeeds but Cortex returns `003001` ("This account is not allowed to access this endpoint"), Snowflake has not enabled Cortex REST for the account. [Self-service trial accounts disable AI features until a credit card is added](https://docs.snowflake.com/en/user-guide/admin-trial-account#label-trial-account-ai-features) (adding one does not end the trial); REST access can remain disabled even after AI SQL functions work, in which case Snowflake support must enable it. Cross-region inference requires an account administrator's explicit decision. `omp models snowflake --json` lists available models only after credentials are configured.
1047
+
1048
+ This provider uses the [public, account-billed Cortex REST API](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-rest-api). CoCo-subscription coverage is unverified. Catalog prices are per-million-token estimates from [Consumption Table 6(b)](https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf) at [$2 per AI Credit](https://docs.snowflake.com/en/user-guide/snowflake-cortex/pricing) for on-demand global routing, not your contract price. Output is capped at the REST API's documented 16,384-token maximum; context windows are conservative AI_COMPLETE limits, not verified REST maxima. Cortex compatibility rules limit explicit cache retention to five minutes. Paste an account identifier or current Snowsight link; legacy `app.snowflake.com/<region>/<locator>` links are rejected because they don't map uniformly to an account host.
1049
+
1035
1050
  ### Vertex AI (ADC)
1036
1051
 
1037
1052
  Vertex AI models use Application Default Credentials (ADC):
@@ -220,6 +220,8 @@ export interface CheckCredentialsOptions {
220
220
  completionProbe?: CompletionProbe;
221
221
  /** Per-credential completion probe timeout (ms). Defaults to `timeoutMs`. */
222
222
  completionTimeoutMs?: number;
223
+ /** Providers whose credentials are skipped entirely (not probed, not reported). */
224
+ excludeProviders?: ReadonlySet<string>;
223
225
  }
224
226
  /**
225
227
  * Sentinel value placed in OAuth `refresh` fields when a credential is shared
@@ -424,6 +426,8 @@ export type AuthApiKeyOptions = {
424
426
  forceRefresh?: boolean;
425
427
  /** Explicit provider-401 recovery; generic force refreshes leave this unset. */
426
428
  refreshReason?: OAuthRefreshReason;
429
+ /** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
430
+ recordAffinity?: boolean;
427
431
  };
428
432
  /** Non-secret identity bound to the OAuth credential selected for one request attempt. */
429
433
  export interface OAuthRequestIdentity {
@@ -20,6 +20,11 @@ export declare class AuthBrokerRefresher {
20
20
  start(): void;
21
21
  stop(): void;
22
22
  getSchedule(): AuthBrokerRefresherSchedule;
23
- /** Run one sweep. Exposed for tests. */
23
+ /**
24
+ * Run one sweep. Exposed for tests.
25
+ *
26
+ * Never rejects: callers fire it unawaited from a timer, so a failed sweep
27
+ * (e.g. an unreadable credential store) is logged and retried next interval.
28
+ */
24
29
  tick(): Promise<void>;
25
30
  }
@@ -43,6 +43,21 @@ export declare function isResponsesRequestBodyReadTimeout(message: {
43
43
  errorMessage?: string;
44
44
  requestBodyReadTimeoutFullReplay?: boolean;
45
45
  }): boolean;
46
+ /**
47
+ * Codex code for the experimental native turn lane refusing a mid-response
48
+ * `response.steer`; the server then drops the in-flight response and closes
49
+ * the socket.
50
+ */
51
+ export declare const CODEX_NATIVE_LANE_STEER_REJECTED_CODE = "unsupported_native_inflight_message";
52
+ /**
53
+ * A Codex turn the native turn lane dropped because omp steered it. The
54
+ * rejection answers our own `response.steer`, not the model's health: the
55
+ * provider stops steering the session, so the same model replays cleanly.
56
+ */
57
+ export declare function isCodexSteerRejection(message: {
58
+ api?: Api;
59
+ errorMessage?: string;
60
+ }): boolean;
46
61
  export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
47
62
  export declare const ANTHROPIC_ACCOUNT_POLICY_PATTERN: RegExp;
48
63
  /** Whether an error message represents an Anthropic account-scoped permission/policy denial. */
@@ -1,4 +1,5 @@
1
- import type { AnthropicOutputEffort, FetchImpl, Message, Model, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types.js";
1
+ import type { Effort } from "@oh-my-pi/pi-catalog/effort";
2
+ import type { AnthropicCompactionRequest, AnthropicOutputEffort, FetchImpl, Message, Model, ServiceTier, SimpleStreamOptions, StreamFunction, StreamOptions, Usage } from "../types.js";
2
3
  import { type AnthropicFetchOptions, type AnthropicMessagesClientLike } from "./anthropic-client.js";
3
4
  import { type FallbackParam, type MessageParam, type TextBlockParam } from "./anthropic-wire.js";
4
5
  import { resolvesToOfficialAnthropicEndpoint, supportsAnthropicCompaction, supportsAnthropicCompactionOnClient } from "./anthropic-compaction.js";
@@ -209,7 +210,7 @@ export type AnthropicSystemBlock = {
209
210
  type SystemBlockOptions = {
210
211
  includeClaudeCodeInstruction?: boolean;
211
212
  extraInstructions?: string[];
212
- /** Text of the first user message — used as fingerprint seed for the billing header. */
213
+ /** Text of the first conversational wire user turn, used as the billing fingerprint seed. */
213
214
  firstUserMessageText?: string;
214
215
  /** Cache lifetime shared by the OAuth system breakpoint and later message breakpoints. */
215
216
  cacheControl?: AnthropicCacheControl;
@@ -234,6 +235,16 @@ export declare function budgetThinkingOutput(maxTokens: number | undefined, budg
234
235
  maxTokens: number;
235
236
  budgetTokens: number;
236
237
  };
238
+ /**
239
+ * The effort whose thinking allowance an on-demand compaction request over
240
+ * `messages` gets. It sends no effort change of its own, so once an earlier
241
+ * request recorded an effort, the one it left in force applies, even when
242
+ * `requested` differs or is off. When that request named none, the API's
243
+ * per-model default applies, which is not known here: the request gets the
244
+ * largest effort's allowance (`max_tokens` is a cap, not spend). Without a
245
+ * recorded effort the request sets its own.
246
+ */
247
+ export declare function resolveAnthropicCompactionEffort(model: Model<"anthropic-messages">, messages: readonly Message[], requested: Effort | undefined): Effort | undefined;
237
248
  /**
238
249
  * Whether the conversation has stopped being thinking-led, so routes with
239
250
  * `compat.stripThinkingHistory` drop the budget `thinking` config and replay
@@ -269,5 +280,7 @@ export declare function convertAnthropicMessages(messages: Message[], model: Mod
269
280
  dropAllThinking?: boolean;
270
281
  droppedThinkingBlocks?: ReadonlySet<string>;
271
282
  credentialId?: number;
283
+ /** On-demand compaction request: it ends inside the conversation, so it gets nothing live requests lack there. */
284
+ compactionRequest?: AnthropicCompactionRequest;
272
285
  }): AnthropicMessageParam[];
273
286
  export declare function normalizeAnthropicToolSchema(schema: unknown): unknown;
@@ -16,9 +16,9 @@ export interface OpenAIResponsesOptions extends StreamOptions {
16
16
  /**
17
17
  * Stateful turns: chain via `previous_response_id` + delta input instead of
18
18
  * replaying the full transcript. Forces `store: true` (the platform only
19
- * resolves stored responses). Defaults ON against the official OpenAI API
20
- * and OFF for other Responses endpoints; `PI_OPENAI_STATEFUL` overrides the
21
- * default, and `false` here vetoes everything. Requires `sessionId` +
19
+ * resolves stored responses). Precedence: this option, then
20
+ * `PI_OPENAI_STATEFUL`, then `compat.statefulResponses`, then ON against the
21
+ * official OpenAI API and OFF elsewhere. Requires `sessionId` +
22
22
  * `providerSessionState`. Falls back to a full replay whenever history
23
23
  * mutates or the server reports a stale id.
24
24
  */
@@ -60,6 +60,27 @@ export interface OpenAIResponsesOptions extends StreamOptions {
60
60
  /** Opt-in GPT-5.6+ prompt-cache policy. Unsupported explicit mode fails locally. */
61
61
  promptCache?: OpenAIPromptCacheOptions;
62
62
  }
63
+ /**
64
+ * Poll a stored Responses result until it reaches an adoptable terminal state.
65
+ * 404s, 5xx, and transport failures mean "not yet" (an in-flight run only
66
+ * becomes retrievable once the server finishes it); any other status, a
67
+ * mismatched id, or a failed/cancelled run ends the poll immediately. The
68
+ * whole recovery is bounded by one wall-clock deadline: each request's timeout
69
+ * is clamped to the time left, and no sleep may cross the deadline, so a
70
+ * stalled host cannot hold an already-failed turn past it. Returns the
71
+ * terminal object, or undefined on give-up/abort.
72
+ *
73
+ * @internal Exported for tests.
74
+ */
75
+ export declare function pollOpenAIResponsesResultForCompletion(args: {
76
+ fetchImpl: NonNullable<StreamOptions["fetch"]>;
77
+ url: string;
78
+ headers: Record<string, string>;
79
+ responseId: string;
80
+ signal?: AbortSignal;
81
+ /** Sleep between polls; defaults to a real wait honoring `signal`. */
82
+ wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
83
+ }): Promise<Record<string, unknown> | undefined>;
63
84
  interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
64
85
  nativeHistoryReplayWarmed: boolean;
65
86
  /** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
@@ -85,6 +106,14 @@ interface OpenAIResponsesChainState {
85
106
  /** Set once chaining is judged unsupported for this session (circuit breaker). */
86
107
  disabled: boolean;
87
108
  }
109
+ /**
110
+ * Set the process-wide storage default per provider id, used when a request
111
+ * leaves `storeResponses` unset. Hosts call this from their settings so every
112
+ * request follows the setting, including direct `streamSimple`/`completeSimple`
113
+ * side calls that do not pass per-request options. `PI_MUSE_STORE_RESPONSES`
114
+ * still overrides it.
115
+ */
116
+ export declare function configureProviderStoreResponses(byProvider: Readonly<Record<string, boolean>> | undefined): void;
88
117
  /**
89
118
  * Drop the account-bound half of every retained `openai-responses` record in
90
119
  * `states`: the stateful `previous_response_id` chain baselines.
@@ -0,0 +1,3 @@
1
+ import type { OAuthController, OAuthCredentials } from "./types.js";
2
+ export declare function loginSnowflake(ctrl: OAuthController): Promise<OAuthCredentials>;
3
+ export declare function refreshSnowflakeToken(credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
@@ -0,0 +1,4 @@
1
+ import type { ProviderTransport } from "./build.js";
2
+ /** Normalize account identifiers and Snowsight links without accepting arbitrary credential-receiving hosts. */
3
+ export declare function normalizeSnowflakeAccountUrl(input: string): string;
4
+ export declare const snowflakeTransport: ProviderTransport;
@@ -215,6 +215,11 @@ export interface CodexCompactionRequestContext extends CodexCompactionMetadata {
215
215
  export interface AnthropicCompactionRequest {
216
216
  /** Custom summarization prompt; replaces the API default entirely when set. */
217
217
  instructions?: string;
218
+ /**
219
+ * Replayed summaries' file metadata due before this time (ms) ends the request;
220
+ * later metadata replays with the retained tail, which the new summary carries.
221
+ */
222
+ filesDueBefore?: number;
218
223
  }
219
224
  /** OpenAI's GPT-5.6+ explicit prompt-cache controls. */
220
225
  export interface OpenAIPromptCacheOptions {
@@ -333,6 +338,16 @@ export interface StreamOptions {
333
338
  * `false` so `previous_response_id` cannot explain a result.
334
339
  */
335
340
  statefulResponses?: boolean;
341
+ /**
342
+ * Store this request's result server-side on hosts that support it
343
+ * (`compat.storeResponses`, e.g. Muse Code), so a stream that drops
344
+ * mid-turn resumes from `GET /responses/{id}` instead of re-running the
345
+ * turn. Privacy: stored runs retain prompts and outputs on the provider.
346
+ * Unset falls back to `PI_MUSE_STORE_RESPONSES`, then the host's
347
+ * `configureProviderStoreResponses` default, else off. Ignored on hosts
348
+ * without the capability.
349
+ */
350
+ storeResponses?: boolean;
336
351
  /**
337
352
  * Disable native reasoning when the caller supplies an external scratchpad.
338
353
  * OpenAI Responses emits `reasoning: { effort: "none" }`; Anthropic and
@@ -363,6 +378,8 @@ export interface StreamOptions {
363
378
  * are not covered.
364
379
  */
365
380
  maxInFlightRequests?: Record<string, number>;
381
+ /** @internal Keep the in-flight permit until a provider's bounded terminal drain finishes. */
382
+ waitForTerminalDrain?: boolean;
366
383
  /**
367
384
  * Optional callback for inspecting or replacing provider payloads before sending.
368
385
  * Return undefined to keep the payload unchanged.
@@ -820,11 +837,31 @@ export interface AnthropicCompactionPayload {
820
837
  encryptedContent?: string;
821
838
  /**
822
839
  * Harness-appended file metadata (`<files>` section) kept out of the
823
- * byte-identical block. Replayed as a user message after the native block:
824
- * the converter replaces the summary message with the block and skips its
825
- * text, so without this the metadata would be invisible to this provider.
840
+ * byte-identical block. The converter replaces the summary message with the
841
+ * block and skips its text, so it replays this as a user message after the
842
+ * block's retained tail: before the first message created after the summary.
826
843
  */
827
844
  filesText?: string;
845
+ /**
846
+ * File metadata of earlier summaries whose replay position lies inside this
847
+ * summary's retained tail. Retained messages must reach the API unchanged,
848
+ * so each keeps replaying where it did: before the first message created
849
+ * after `after` (the earlier summary's commit time).
850
+ */
851
+ retainedFiles?: AnthropicCompactionFiles[];
852
+ /**
853
+ * Set on summaries whose retained tail replays unchanged: file metadata
854
+ * after the tail, earlier metadata at `retainedFiles`. Summaries persisted
855
+ * without it keep their original layout (metadata after the first retained
856
+ * turn), since later thinking was signed against those bytes.
857
+ */
858
+ exactTail?: true;
859
+ }
860
+ /** File metadata replayed at a fixed point of a natively compacted conversation. */
861
+ export interface AnthropicCompactionFiles {
862
+ text: string;
863
+ /** Replays before the first message created after this time (ms). */
864
+ after: number;
828
865
  }
829
866
  export type ProviderPayload = OpenAIResponsesHistoryPayload | AnthropicMessagePayload | AnthropicCompactionPayload;
830
867
  /** Provider-reported rewrite applied to request content before inference. */
@@ -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>;
@@ -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}. */
@@ -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": "@oh-my-pi/pi-ai",
3
- "version": "18.6.0",
3
+ "version": "18.6.3",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.6.0",
159
- "@oh-my-pi/pi-catalog": "18.6.0",
160
- "@oh-my-pi/pi-natives": "18.6.0",
161
- "@oh-my-pi/pi-utils": "18.6.0",
162
- "@oh-my-pi/pi-wire": "18.6.0"
158
+ "@oh-my-pi/omptype": "18.6.3",
159
+ "@oh-my-pi/pi-catalog": "18.6.3",
160
+ "@oh-my-pi/pi-natives": "18.6.3",
161
+ "@oh-my-pi/pi-utils": "18.6.3",
162
+ "@oh-my-pi/pi-wire": "18.6.3"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -356,7 +356,9 @@ export class KeyCascade implements KeysApi {
356
356
  credential => credential.source === "login" && !this.isKeylessFallback(provider, credential),
357
357
  );
358
358
  if (loginApiKeySelection) {
359
- this.#deps.affinity.record(provider, sessionId, "api_key", loginApiKeySelection.index);
359
+ if (options?.recordAffinity !== false) {
360
+ this.#deps.affinity.record(provider, sessionId, "api_key", loginApiKeySelection.index);
361
+ }
360
362
  const credentialId = onCredentialId
361
363
  ? this.#deps.pool.entries(provider)[loginApiKeySelection.index]?.id
362
364
  : undefined;
@@ -382,7 +384,9 @@ export class KeyCascade implements KeysApi {
382
384
  credential => credential.source !== "login",
383
385
  );
384
386
  if (apiKeySelection) {
385
- this.#deps.affinity.record(provider, sessionId, "api_key", apiKeySelection.index);
387
+ if (options?.recordAffinity !== false) {
388
+ this.#deps.affinity.record(provider, sessionId, "api_key", apiKeySelection.index);
389
+ }
386
390
  const credentialId = onCredentialId ? this.#deps.pool.entries(provider)[apiKeySelection.index]?.id : undefined;
387
391
  const apiKey = await this.#deps.overrides.resolve(apiKeySelection.credential.key);
388
392
  if (apiKey !== undefined && credentialId !== undefined) onCredentialId?.(credentialId);
@@ -283,6 +283,7 @@ export class CredentialHealth implements HealthApi {
283
283
  const results: CredentialHealthResult[] = [];
284
284
  for (const row of stored) {
285
285
  options?.signal?.throwIfAborted();
286
+ if (options?.excludeProviders?.has(row.provider)) continue;
286
287
  const base: CredentialHealthResult = {
287
288
  id: row.id,
288
289
  provider: row.provider,
@@ -1055,7 +1055,9 @@ export class CredentialSelector {
1055
1055
  }
1056
1056
  }
1057
1057
  this.#deps.pool.noteBearer(provider, result.apiKey, credentialId);
1058
- this.#deps.affinity.record(provider, sessionId, "oauth", selection.index);
1058
+ if (options?.recordAffinity !== false) {
1059
+ this.#deps.affinity.record(provider, sessionId, "oauth", selection.index);
1060
+ }
1059
1061
  return { apiKey: result.apiKey, credential: updated, credentialId };
1060
1062
  } catch (error) {
1061
1063
  const errorMsg = String(error);
package/src/auth/types.ts CHANGED
@@ -257,6 +257,8 @@ export interface CheckCredentialsOptions {
257
257
  completionProbe?: CompletionProbe;
258
258
  /** Per-credential completion probe timeout (ms). Defaults to `timeoutMs`. */
259
259
  completionTimeoutMs?: number;
260
+ /** Providers whose credentials are skipped entirely (not probed, not reported). */
261
+ excludeProviders?: ReadonlySet<string>;
260
262
  }
261
263
 
262
264
  // ─────────────────────────────────────────────────────────────────────────────
@@ -487,6 +489,8 @@ export type AuthApiKeyOptions = {
487
489
  forceRefresh?: boolean;
488
490
  /** Explicit provider-401 recovery; generic force refreshes leave this unset. */
489
491
  refreshReason?: OAuthRefreshReason;
492
+ /** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
493
+ recordAffinity?: boolean;
490
494
  };
491
495
 
492
496
  /** Non-secret identity bound to the OAuth credential selected for one request attempt. */
@@ -75,7 +75,12 @@ export class AuthBrokerRefresher {
75
75
  };
76
76
  }
77
77
 
78
- /** Run one sweep. Exposed for tests. */
78
+ /**
79
+ * Run one sweep. Exposed for tests.
80
+ *
81
+ * Never rejects: callers fire it unawaited from a timer, so a failed sweep
82
+ * (e.g. an unreadable credential store) is logged and retried next interval.
83
+ */
79
84
  async tick(): Promise<void> {
80
85
  if (this.#running) return;
81
86
  this.#running = true;
@@ -94,6 +99,8 @@ export class AuthBrokerRefresher {
94
99
  targets.push(entry.id);
95
100
  }
96
101
  await Promise.all(targets.map(id => this.#refreshOne(id)));
102
+ } catch (error) {
103
+ logger.warn("auth-broker refresh sweep failed", { error: String(error) });
97
104
  } finally {
98
105
  this.#running = false;
99
106
  this.#nextSweepAt = this.#now() + this.#refreshIntervalMs;
@@ -128,6 +128,8 @@ export class DeepSeekInbandScanner implements InbandScanner {
128
128
  #dsmlParamName = "";
129
129
  #dsmlParamIsString = true;
130
130
  #dsmlParamRaw = "";
131
+ /** Unclassified wrapper text is kept until an invoke proves it is a tool call. */
132
+ #pendingDsmlSection: string | undefined;
131
133
  #rawBlock = "";
132
134
  #stripLeadingWhitespace = false;
133
135
  /**
@@ -194,6 +196,12 @@ export class DeepSeekInbandScanner implements InbandScanner {
194
196
  if (!this.#consumeDsmlParam(final, events)) break;
195
197
  }
196
198
  if (final && this.#state === "thinking") this.#endThinking(events);
199
+ if (final && this.#pendingDsmlSection !== undefined) {
200
+ this.#emitText(this.#pendingDsmlSection + this.#buffer, events);
201
+ this.#pendingDsmlSection = undefined;
202
+ this.#buffer = "";
203
+ this.#state = "outside";
204
+ }
197
205
  if (final && this.#buffer.length === 0 && this.#rawBlock.length > 0) this.#rawBlock = "";
198
206
  return events;
199
207
  }
@@ -249,6 +257,7 @@ export class DeepSeekInbandScanner implements InbandScanner {
249
257
  ? DSML_TOOL_CALLS_OPEN_FULLWIDTH
250
258
  : DSML_TOOL_CALLS_OPEN_ASCII;
251
259
  this.#buffer = this.#buffer.slice(openToken.length);
260
+ this.#pendingDsmlSection = openToken;
252
261
  this.#state = "dsmlSection";
253
262
  return;
254
263
  }
@@ -381,16 +390,25 @@ export class DeepSeekInbandScanner implements InbandScanner {
381
390
  }
382
391
 
383
392
  #consumeDsmlSection(final: boolean, events: InbandScanEvent[]): boolean {
393
+ const initial = this.#buffer;
384
394
  while (this.#buffer.length > 0) {
385
395
  this.#skipWhitespace();
386
396
  const close = this.#matchingDsmlClose(DSML_TOOL_CALLS_CLOSE_FULLWIDTH, DSML_TOOL_CALLS_CLOSE_ASCII);
387
397
  if (close) {
398
+ if (this.#pendingDsmlSection !== undefined) {
399
+ this.#emitText(
400
+ this.#pendingDsmlSection + initial.slice(0, initial.length - this.#buffer.length) + close,
401
+ events,
402
+ );
403
+ this.#pendingDsmlSection = undefined;
404
+ }
388
405
  this.#buffer = this.#buffer.slice(close.length);
389
406
  this.#state = "outside";
390
407
  return true;
391
408
  }
392
409
  const invoke = this.#matchDsmlOpen("invoke");
393
410
  if (invoke) {
411
+ this.#pendingDsmlSection = undefined;
394
412
  this.#rawBlock = invoke.raw;
395
413
  this.#name = invoke.name;
396
414
  this.#id = mintToolCallId();
@@ -404,12 +422,15 @@ export class DeepSeekInbandScanner implements InbandScanner {
404
422
  (this.#buffer.startsWith("<|DSML|invoke") || this.#buffer.startsWith("<|DSML|invoke")) &&
405
423
  !this.#buffer.includes(">")
406
424
  )
407
- return false;
408
- if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) return false;
425
+ break;
426
+ if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) break;
409
427
  }
410
- if (this.#buffer.length === 0) return false;
428
+ if (this.#buffer.length === 0) break;
411
429
  this.#buffer = this.#buffer.slice(1);
412
430
  }
431
+ if (this.#pendingDsmlSection !== undefined) {
432
+ this.#pendingDsmlSection += initial.slice(0, initial.length - this.#buffer.length);
433
+ }
413
434
  return final;
414
435
  }
415
436