@oh-my-pi/pi-ai 18.14.1 → 18.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +37 -60
  2. package/dist/types/auth/refresh.d.ts +13 -0
  3. package/dist/types/auth/types.d.ts +11 -5
  4. package/dist/types/auth-storage.d.ts +9 -4
  5. package/dist/types/providers/anthropic.d.ts +5 -0
  6. package/dist/types/providers/factory-droid.d.ts +2 -0
  7. package/dist/types/providers/google-auth.d.ts +2 -3
  8. package/dist/types/providers/openai-chat-server.d.ts +7 -1
  9. package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -0
  10. package/dist/types/providers/openai-completions.d.ts +13 -0
  11. package/dist/types/providers/openai-responses-server-schema.d.ts +12 -12
  12. package/dist/types/types.d.ts +6 -2
  13. package/dist/types/utils/retry-after.d.ts +3 -1
  14. package/dist/types/utils/thinking-loop.d.ts +14 -9
  15. package/dist/types/utils.d.ts +5 -0
  16. package/package.json +6 -6
  17. package/src/auth/refresh.ts +49 -11
  18. package/src/auth/types.ts +11 -5
  19. package/src/auth/usage.ts +1 -0
  20. package/src/auth-broker/client.ts +1 -1
  21. package/src/auth-broker/server.ts +7 -2
  22. package/src/auth-gateway/server.ts +1 -0
  23. package/src/auth-storage.ts +18 -6
  24. package/src/providers/anthropic.ts +42 -4
  25. package/src/providers/factory-droid.ts +65 -1
  26. package/src/providers/google-auth.ts +57 -13
  27. package/src/providers/openai-chat-server.ts +69 -9
  28. package/src/providers/openai-codex/request-transformer.ts +8 -8
  29. package/src/providers/openai-completions.ts +25 -0
  30. package/src/providers/openai-responses-server-schema.ts +5 -5
  31. package/src/providers/openai-responses-server.ts +3 -3
  32. package/src/providers/openai-shared.ts +6 -5
  33. package/src/stream.ts +1 -0
  34. package/src/types.ts +11 -3
  35. package/src/usage/cursor.ts +7 -1
  36. package/src/utils/http-inspector.ts +47 -14
  37. package/src/utils/retry-after.ts +10 -3
  38. package/src/utils/thinking-loop.ts +38 -27
  39. package/src/utils.ts +15 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.15.0] - 2026-11-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Codex conversation replay failing with an unknown `internal_chat_message_metadata_passthrough.content_token_count` parameter by omitting backend-internal response metadata from replayed input ([#17590](https://github.com/can1357/oh-my-pi/pull/17590), [#17597](https://github.com/can1357/oh-my-pi/pull/17597) by [@briankeefe](https://github.com/briankeefe)).
10
+ - Fixed `service_tier: "ultrafast"` being dropped for OpenAI-family models on custom providers even when the model's `serviceTiers` lists it ([#17577](https://github.com/can1357/oh-my-pi/pull/17577) by [@jakehemmerle](https://github.com/jakehemmerle))
11
+ - Fixed Google Vertex requests with Workforce Identity Federation ADC failing token refresh against the wrong endpoint ([#17447](https://github.com/can1357/oh-my-pi/issues/17447)).
12
+ - Fixed Anthropic (and other rotating-token) OAuth accounts being signed out automatically when a background usage poll's token refresh got `invalid_grant`, including refreshes a client delegates to an auth broker; usage polling no longer disables credentials, so only a real request's refresh can do that ([#17468](https://github.com/can1357/oh-my-pi/issues/17468))
13
+
14
+ ## [18.14.2] - 2026-10-31
15
+
16
+ ### Breaking Changes
17
+
18
+ - Replaced `isLoopGuardedModel` with `loopGuardLevel`, which returns `"off"`, `"exact"`, or `"semantic"` from the model's `thinking-loop-guard` catalog rule
19
+
20
+ ### Added
21
+
22
+ - Added `disableServerSideFallback` to `FactoryDroidOptions` and `SimpleStreamOptions`: a Factory Droid request leaves out the server-side refusal fallback chain and fails, before any of its content is streamed, if another model answers it ([#12229](https://github.com/can1357/oh-my-pi/pull/12229) by [@Xytronix](https://github.com/Xytronix))
23
+
24
+ ### Changed
25
+
26
+ - DeepSeek and Grok models on Amazon Bedrock now get the same near-duplicate reasoning-loop checks as on other hosts
27
+
28
+ ### Fixed
29
+
30
+ - Fixed Claude turns failing with "Thinking loop detected" when summarized thinking repeated a progress line such as "Still writing the parser..."; Claude models no longer run the loop guard
31
+ - Fixed the loop guard discarding turns from other models whose reasoning summary repeats one status line a few times; whole-line repetition now has to cover about 1 KB before it counts as a loop
32
+ - Fixed per-request OpenAI-compatible headers being combined with differently cased model headers instead of overriding them ([#17513](https://github.com/can1357/oh-my-pi/pull/17513) by [@jchanghong023](https://github.com/jchanghong023))
33
+ - Fixed a stalled Anthropic stream that keeps sending empty content deltas never timing out, because those deltas were treated as stream progress ([#17484](https://github.com/can1357/oh-my-pi/pull/17484) by [@jchanghong023](https://github.com/jchanghong023))
34
+ - Fixed Anthropic-compatible endpoints that put a thinking signature on the streamed content block losing it, so replayed reasoning keeps its signed block ([#17483](https://github.com/can1357/oh-my-pi/pull/17483) by [@jchanghong023](https://github.com/jchanghong023))
35
+ - Fixed `AuthStorage.close()` and `replaceStore()` losing a rotated OAuth refresh token when a refresh was still in flight, with or without durable refresh leases; the store now stays open until that token is saved, and new refreshes are refused ([#17518](https://github.com/can1357/oh-my-pi/issues/17518))
36
+ - Fixed `/usage` showing Cursor's "Other Models" meter as an inflated dollar amount; it now shows the percentage Cursor reports, like Cursor Models ([#17462](https://github.com/can1357/oh-my-pi/pull/17462) by [@eggpeat](https://github.com/eggpeat))
37
+ - Fixed the `omp auth-gateway` Responses endpoint rejecting documented null values for temperature, `top_p`, output limits, reasoning, and reasoning summaries ([#17466](https://github.com/can1357/oh-my-pi/pull/17466) by [@Xytronix](https://github.com/Xytronix)).
38
+ - Fixed provider HTTP errors printing the response body twice when the thrown error already carried it, including responses with a retry header ([#17503](https://github.com/can1357/oh-my-pi/pull/17503) by [@metaphoric-dev](https://github.com/metaphoric-dev))
39
+ - Fixed Gemini tool calls through the `omp auth-gateway` chat-completions endpoint failing on the next turn: the gateway now returns each call's thought signature in `extra_content` and replays the one a client sends back ([#17509](https://github.com/can1357/oh-my-pi/pull/17509) by [@justdoGIT](https://github.com/justdoGIT))
40
+
5
41
  ## [18.14.1] - 2026-10-31
6
42
 
7
43
  ### Added
@@ -2260,63 +2296,4 @@
2260
2296
  - Fixed an issue where malformed tool-call JSON from local Ollama or llama.cpp models was incorrectly retried as generic 500 errors, now surfacing a clear recovery message.
2261
2297
  - Fixed a race condition in OAuth callback flows where abort signals triggered before the callback listener was registered were ignored.
2262
2298
 
2263
- ## [16.2.7] - 2026-06-30
2264
-
2265
- ### Added
2266
-
2267
- - Added service tier support for Google Gemini and Vertex AI, including model-specific service tier configurations via ServiceTierByFamily.
2268
- - Added Google Vertex AI Interactions API support for Gemini 3+ models by default, with automatic fallback to :streamGenerateContent and a useInteractionsApi: false option to force standard generation.
2269
- - Added support for explicit Vertex bearer access tokens via GOOGLE_CLOUD_ACCESS_TOKEN or CLOUDSDK_AUTH_ACCESS_TOKEN environment variables.
2270
-
2271
- ### Changed
2272
-
2273
- - Updated service tier logic to use per-provider configurations instead of global scopes.
2274
- - Refactored priority request billing and accounting to better align with specific provider capabilities.
2275
- - Updated API key resolution precedence so explicit environment variables (e.g., GEMINI_API_KEY) override stored or broker-migrated static API keys, while deliberate OAuth logins still take highest precedence.
2276
-
2277
- ### Fixed
2278
-
2279
- - Improved Vertex AI reliability by automatically falling back to global endpoints on 404 errors.
2280
- - Fixed safety setting application for Google Vertex AI models.
2281
- - Fixed Kimi Code's Anthropic-compatible request path to keep thinking enabled and downgrade forced tool choice for Kimi K2.7 Code title generation.
2282
- - Fixed leaked reasoning fences (such as ```thinking or <think>) across all providers by splitting them into structured thinking blocks during streaming.
2283
- - Fixed Codex requests failing with unsupported all_turns errors on older models (gpt-5.1 and gpt-5.3) by gating the reasoning.context: "all_turns" default to gpt-5.4+ models.
2284
-
2285
- ## [16.2.6] - 2026-06-29
2286
-
2287
- ### Fixed
2288
-
2289
- - Fixed Antigravity usage reporting to correctly infer daily and weekly quota windows from unlabeled reset-only rows, preventing Cloud Code Assist payloads from collapsing these counters into the default category.
2290
-
2291
- ## [16.2.5] - 2026-06-28
2292
-
2293
- ### Fixed
2294
-
2295
- - Fixed Google and Cloud Code Assist streams that end without a finish reason (dropped connections or truncated responses) being treated as fatal; they are now classified as transient so the coding agent automatically retries.
2296
-
2297
- ## [16.2.4] - 2026-06-28
2298
-
2299
- ### Added
2300
-
2301
- - Enabled freeform tool patch support for Azure OpenAI and Codex models
2302
-
2303
- ### Fixed
2304
-
2305
- - Fixed usage reporting for Antigravity and Z.AI to correctly surface and preserve distinct quota windows (daily, weekly, monthly) instead of collapsing or duplicating them
2306
- - Fixed an issue where `/usage show` returned "No usage data available" when using a custom proxy base URL for Codex
2307
- - Fixed OpenAI stream read errors being incorrectly classified as non-transient, enabling the coding agent to automatically retry after recoverable stream failures
2308
-
2309
- ## [16.2.3] - 2026-06-28
2310
-
2311
- ### Changed
2312
-
2313
- - Enabled automatic removal of leaked reasoning tags for all models
2314
- - Prevented reasoning text duplication when models emit both structured and inline thinking
2315
- - Defaulted reasoning context to all turns for all Codex requests.
2316
-
2317
- ### Fixed
2318
-
2319
- - Enabled freeform tool patch support for Azure OpenAI and Codex models.
2320
- - Fixed an issue where the `/usage show` command returned "No usage data available" when using a custom proxy base URL for Codex.
2321
-
2322
- Older entries are archived in [packages/ai/CHANGELOG.md@cef4a452836c](https://github.com/can1357/oh-my-pi/blob/cef4a452836c7a8d547dd14470c34c9ed2dbb5f8/packages/ai/CHANGELOG.md).
2299
+ Older entries are archived in [packages/ai/CHANGELOG.md@7dc0d0325754](https://github.com/can1357/oh-my-pi/blob/7dc0d0325754955b01c15b64b220b2cf27e670db/packages/ai/CHANGELOG.md).
@@ -40,6 +40,14 @@ export declare class OAuthRefresher {
40
40
  * Refresh one stored OAuth credential under durable row ownership.
41
41
  */
42
42
  refreshStored<T extends OAuthCredential = OAuthCredential>(provider: string, options: StoredOAuthRefreshOptions<T>): Promise<StoredOAuthRefreshResult<T>>;
43
+ /**
44
+ * Stop admitting refreshes, then run `closeStore` once tracked work settles:
45
+ * a token exchange already under way, its save, a lease hand-back. Runs it at
46
+ * once when nothing is in flight. Closing the store under that work would
47
+ * lose a refresh token the provider already rotated. Called by
48
+ * {@link AuthStorage.close} and {@link AuthStorage.replaceStore}.
49
+ */
50
+ close(closeStore: () => void): void;
43
51
  /**
44
52
  * Handles a definitively-dead OAuth grant discovered during refresh (`invalid_grant`,
45
53
  * `revoked`, …): checks for a peer rotation that raced the failure, then CAS-disables
@@ -54,6 +62,11 @@ export declare class OAuthRefresher {
54
62
  * continuing.
55
63
  */
56
64
  disableDefinitiveFailure(provider: string, credentialId: number | undefined, attemptedCredential: OAuthCredential, index: number, errorMsg: string): Promise<"disabled" | "peer-rotated" | "cas-lost">;
65
+ /**
66
+ * Refresh `credential` unless it is still fresh. Shutdown waits for the
67
+ * refresh, and for the caller's synchronous save of its result, before the
68
+ * store closes.
69
+ */
57
70
  refresh(provider: Provider, credential: OAuthCredential, credentialId: number | undefined, signal?: AbortSignal, reason?: OAuthRefreshReason, rejectedAccess?: string): Promise<OAuthCredentials>;
58
71
  /**
59
72
  * Refresh the OAuth credential with the given id through a per-credential
@@ -279,8 +279,13 @@ export interface CredentialDisabledEvent {
279
279
  orgId?: string;
280
280
  orgName?: string;
281
281
  }
282
- /** Why a delegated OAuth refresh was requested. */
283
- export type OAuthRefreshReason = "auth-recovery";
282
+ /**
283
+ * Why an OAuth refresh was requested.
284
+ * - `auth-recovery`: a provider rejected the bearer with a 401.
285
+ * - `usage-probe`: a background usage poll; advisory, so a definitive refresh
286
+ * failure never disables the credential (an interactive request decides).
287
+ */
288
+ export type OAuthRefreshReason = "auth-recovery" | "usage-probe";
284
289
  /** Configuration supplied when constructing credential storage. */
285
290
  export type AuthStorageOptions = {
286
291
  usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
@@ -979,9 +984,10 @@ export interface OAuthRefreshByIdOptions {
979
984
  */
980
985
  reuseRecentMint?: boolean;
981
986
  /**
982
- * Provider-401 recovery intent. Forwarded to a delegated refresh (an auth
983
- * broker), which may then return a token it minted in the last five minutes.
984
- * Local reuse still requires {@link reuseRecentMint}.
987
+ * Why the refresh was requested. `auth-recovery` is forwarded to a delegated
988
+ * refresh (an auth broker), which may then return a token it minted in the
989
+ * last five minutes; local reuse still requires {@link reuseRecentMint}.
990
+ * `usage-probe` never disables the row on a definitive refresh failure.
985
991
  */
986
992
  reason?: OAuthRefreshReason;
987
993
  /** The access token a provider 401 rejected; see {@link AuthApiKeyOptions.rejectedAccessToken}. */
@@ -46,9 +46,10 @@ export declare class AuthStorage {
46
46
  /**
47
47
  * Swap the backing credential store in place (live `auth.broker.url` change).
48
48
  * Loads `store` into fresh store-bound state — pins, blocks, and usage caches are
49
- * keyed by the old store's row ids — then closes the previous store. Runtime key
50
- * overrides, account policies, session account restrictions, usage-provider
51
- * overrides, and credential event subscribers carry over. On a load failure
49
+ * keyed by the old store's row ids — then retires the previous store like
50
+ * {@link AuthStorage.close}. Runtime key overrides, account policies, session
51
+ * account restrictions, usage-provider overrides, and credential event
52
+ * subscribers carry over. On a load failure
52
53
  * `store` is closed and the current store stays active.
53
54
  */
54
55
  replaceStore(store: AuthCredentialStore, options?: {
@@ -56,7 +57,11 @@ export declare class AuthStorage {
56
57
  }): Promise<void>;
57
58
  /** Open the SQLite store at `dbPath` and wrap it (standalone use, e.g. the pi-ai CLI). */
58
59
  static create(dbPath: string, options?: AuthStorageOptions): Promise<AuthStorage>;
59
- /** Close the underlying credential store; the instance must not be reused. */
60
+ /**
61
+ * Close the underlying credential store; the instance must not be reused.
62
+ * New OAuth refreshes are refused at once; a token exchange already under way
63
+ * keeps the store open until it has saved the rotated token.
64
+ */
60
65
  close(): void;
61
66
  /**
62
67
  * Legacy redirect for callers of the pre-namespace flat API (e.g. repo scripts).
@@ -17,6 +17,11 @@ export * from "./claude-code-fingerprint.js";
17
17
  * pass through untouched, so installing it on every OAuth flow is safe.
18
18
  */
19
19
  export declare function wrapFetchForCch(base: FetchImpl): FetchImpl;
20
+ /**
21
+ * Whether a request to `model` sends the tool `name` for the host to run itself, never dispatching the call to the
22
+ * client: the same decision `encodeAnthropicToolName` makes on the wire.
23
+ */
24
+ export declare function anthropicHostRunsTool(model: Model<"anthropic-messages">, name: string, headers?: Record<string, string>): boolean;
20
25
  export type AnthropicEffort = AnthropicOutputEffort | "adaptive";
21
26
  export type AnthropicThinkingDisplay = "summarized" | "omitted";
22
27
  export interface AnthropicOptions extends StreamOptions {
@@ -3,6 +3,8 @@ import { type FactoryDroidWire } from "@oh-my-pi/pi-catalog/wire/factory-droid";
3
3
  import type { OAuthRequestIdentity } from "../auth/types.js";
4
4
  import type { Api, Model, ServiceTier, StreamFunction, StreamOptions, ToolChoice } from "../types.js";
5
5
  export interface FactoryDroidOptions extends StreamOptions {
6
+ /** Per-call workers only: send no refusal fallbacks or fallback-credit beta, and fail a response served by another model. */
7
+ disableServerSideFallback?: boolean;
6
8
  reasoning?: Effort;
7
9
  disableReasoning?: boolean;
8
10
  toolChoice?: ToolChoice;
@@ -3,10 +3,9 @@
3
3
  *
4
4
  * Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
5
5
  * Sources, in priority order:
6
- * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT exchange)
7
- * or `type: "authorized_user"` (refresh-token exchange).
6
+ * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → service account, user, or workforce ADC file.
8
7
  * 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
9
- * `~/.config/gcloud/application_default_credentials.json` elsewhere (authorized_user flow).
8
+ * `~/.config/gcloud/application_default_credentials.json` elsewhere.
10
9
  * 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
11
10
  *
12
11
  * Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
@@ -3,9 +3,15 @@
3
3
  * `stream(model, context, options)`.
4
4
  */
5
5
  import type { AuthGatewayStreamControl, AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types.js";
6
- import type { AssistantMessage, AssistantMessageEventStream } from "../types.js";
6
+ import type { AssistantMessage, AssistantMessageEventStream, Context, Model } from "../types.js";
7
7
  export type { ParsedRequest };
8
8
  export declare function parseRequest(body: unknown, headers?: Headers): ParsedRequest;
9
+ /**
10
+ * Stamp the replayed tool-call turns that carry a Gemini signature as `model`'s own: they came from this gateway
11
+ * model, and a turn labeled as a foreign model loses its signature in `transformMessages`. Native Gemini
12
+ * transports replay the raw signature; every other transport keeps the stored envelope.
13
+ */
14
+ export declare function adoptReplayedGeminiSignatures(context: Context, model: Model): void;
9
15
  export declare function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown>;
10
16
  export declare function encodeStream(events: AssistantMessageEventStream, requestedModelId: string, options?: ParsedRequest["options"], control?: AuthGatewayStreamControl): ReadableStream<Uint8Array>;
11
17
  /**
@@ -31,6 +31,7 @@ export interface InputItem {
31
31
  type?: string | null;
32
32
  role?: string;
33
33
  content?: unknown;
34
+ internal_chat_message_metadata_passthrough?: unknown;
34
35
  call_id?: string | null;
35
36
  name?: string;
36
37
  output?: unknown;
@@ -3,6 +3,19 @@ import type { AssistantMessage, Context, Model, ServiceTier, StreamFunction, Str
3
3
  import type { ChatCompletionMessageParam } from "./openai-chat-wire.js";
4
4
  import { type OpenAIPromptCacheOptions } from "./openai-shared.js";
5
5
  export { applyOpenRouterRoutingVariant } from "./openai-shared.js";
6
+ type GeminiThoughtSignatureNamespace = "google" | "vertex";
7
+ type GeminiThoughtSignatureExtraContent = Partial<Record<GeminiThoughtSignatureNamespace, {
8
+ thought_signature: string;
9
+ }>>;
10
+ /** The stored signature envelope for a client-replayed `extra_content` (`google` or `vertex`), as this provider writes it. */
11
+ export declare function storeGeminiToolCallSignature(extraContent: unknown): string | undefined;
12
+ /**
13
+ * The `extra_content` that replays a tool call's Gemini signature on an OpenAI-compatible wire: read from the
14
+ * envelope this provider stores, or wrapped from the raw signature a native Gemini transport recorded.
15
+ */
16
+ export declare function geminiToolCallExtraContent(thoughtSignature: string | undefined): GeminiThoughtSignatureExtraContent | undefined;
17
+ /** The raw Gemini signature behind a tool call's `thoughtSignature`, as native Gemini parts carry it. */
18
+ export declare function rawGeminiToolCallSignature(thoughtSignature: string | undefined): string | undefined;
6
19
  /**
7
20
  * Whether a thinking block's signature is a raw LiteLLM (Anthropic) one, not a
8
21
  * reasoning field name or the Mistral content-parts marker.
@@ -853,10 +853,10 @@ export declare const toolChoiceSchema: import("@oh-my-pi/omptype").FluentType<"a
853
853
  }>;
854
854
  export declare const reasoningConfigSchema: import("@oh-my-pi/omptype").FluentType<{
855
855
  effort?: string | undefined;
856
- summary?: "auto" | "concise" | "detailed" | "none" | undefined;
856
+ summary?: "auto" | "concise" | "detailed" | "none" | null | undefined;
857
857
  }, {
858
858
  effort?: string | undefined;
859
- summary?: "auto" | "concise" | "detailed" | "none" | undefined;
859
+ summary?: "auto" | "concise" | "detailed" | "none" | null | undefined;
860
860
  }>;
861
861
  export declare const stopSchema: import("@oh-my-pi/omptype").FluentType<string | string[] | null, string | string[] | null>;
862
862
  export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").FluentType<{
@@ -1101,7 +1101,7 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1101
1101
  type: string;
1102
1102
  })[] | undefined;
1103
1103
  instructions?: string | null | undefined;
1104
- max_output_tokens?: number | undefined;
1104
+ max_output_tokens?: number | null | undefined;
1105
1105
  metadata?: unknown;
1106
1106
  model: string;
1107
1107
  parallel_tool_calls?: boolean | undefined;
@@ -1111,14 +1111,14 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1111
1111
  prompt_cache_key?: string | undefined;
1112
1112
  reasoning?: {
1113
1113
  effort?: string | undefined;
1114
- summary?: "auto" | "concise" | "detailed" | "none" | undefined;
1115
- } | undefined;
1114
+ summary?: "auto" | "concise" | "detailed" | "none" | null | undefined;
1115
+ } | null | undefined;
1116
1116
  safety_identifier?: unknown;
1117
1117
  service_tier?: string | undefined;
1118
1118
  stop?: string | string[] | null | undefined;
1119
1119
  store?: boolean | undefined;
1120
1120
  stream?: boolean | undefined;
1121
- temperature?: number | undefined;
1121
+ temperature?: number | null | undefined;
1122
1122
  text?: unknown;
1123
1123
  tool_choice?: "auto" | "none" | "required" | {
1124
1124
  name: string;
@@ -1148,7 +1148,7 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1148
1148
  type: string;
1149
1149
  })[] | undefined;
1150
1150
  top_logprobs?: unknown;
1151
- top_p?: number | undefined;
1151
+ top_p?: number | null | undefined;
1152
1152
  truncation?: unknown;
1153
1153
  user?: string | undefined;
1154
1154
  }, {
@@ -1393,7 +1393,7 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1393
1393
  type: string;
1394
1394
  })[] | undefined;
1395
1395
  instructions?: string | null | undefined;
1396
- max_output_tokens?: number | undefined;
1396
+ max_output_tokens?: number | null | undefined;
1397
1397
  metadata?: unknown;
1398
1398
  model: string;
1399
1399
  parallel_tool_calls?: boolean | undefined;
@@ -1403,14 +1403,14 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1403
1403
  prompt_cache_key?: string | undefined;
1404
1404
  reasoning?: {
1405
1405
  effort?: string | undefined;
1406
- summary?: "auto" | "concise" | "detailed" | "none" | undefined;
1407
- } | undefined;
1406
+ summary?: "auto" | "concise" | "detailed" | "none" | null | undefined;
1407
+ } | null | undefined;
1408
1408
  safety_identifier?: unknown;
1409
1409
  service_tier?: string | undefined;
1410
1410
  stop?: string | string[] | null | undefined;
1411
1411
  store?: boolean | undefined;
1412
1412
  stream?: boolean | undefined;
1413
- temperature?: number | undefined;
1413
+ temperature?: number | null | undefined;
1414
1414
  text?: unknown;
1415
1415
  tool_choice?: "auto" | "none" | "required" | {
1416
1416
  name: string;
@@ -1440,7 +1440,7 @@ export declare const openaiResponsesRequestSchema: import("@oh-my-pi/omptype").F
1440
1440
  type: string;
1441
1441
  })[] | undefined;
1442
1442
  top_logprobs?: unknown;
1443
- top_p?: number | undefined;
1443
+ top_p?: number | null | undefined;
1444
1444
  truncation?: unknown;
1445
1445
  user?: string | undefined;
1446
1446
  }>;
@@ -144,8 +144,10 @@ export declare function resolveModelServiceTier(tiers: ServiceTierByFamily | nul
144
144
  * `service_tier_for_request`); an empty or missing list counts as "not
145
145
  * reported" — accounts whose `/models` lists no tiers keep `/fast` — so the
146
146
  * provider-level answer stands. `flex` and `default` are never gated.
147
- * First-party OpenAI takes `ultrafast` as-is. A bare provider string cannot
148
- * carry the list, so it answers for the provider alone.
147
+ * First-party OpenAI takes `ultrafast` as-is. An OpenAI-family relay on the
148
+ * Responses/Completions APIs gets `ultrafast` only when its discovered
149
+ * `service_tiers` lists it. A bare provider string cannot carry the list, so
150
+ * it answers for the provider alone.
149
151
  */
150
152
  export declare function shouldSendServiceTier(serviceTier: ServiceTier | null | undefined, target: Provider | ServiceTierModel | undefined): boolean;
151
153
  /**
@@ -631,6 +633,8 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
631
633
  * provider. Non-Anthropic providers ignore the field.
632
634
  */
633
635
  fallbacks?: FallbackParam[];
636
+ /** Factory Droid only: omit refusal fallbacks and reject responses served by another model. */
637
+ disableServerSideFallback?: boolean;
634
638
  }
635
639
  export type StreamFunction<TApi extends Api> = (model: Model<TApi>, context: Context, options: OptionsForApi<TApi>) => AssistantMessageEventStream;
636
640
  export interface TextSignatureV1 {
@@ -1,6 +1,8 @@
1
1
  export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
2
2
  /** Extracts retry timing using the provider's catalog-declared timestamp timezone. */
3
3
  export declare function extractProviderRetryHint(provider: string | undefined, message: string | undefined): number | undefined;
4
- export declare function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string;
4
+ export declare function errorMessageText(error: unknown): string;
5
+ export declare function appendRetryAfterHint(message: string, headers: HeadersLike): string;
6
+ export declare function formatErrorMessageWithRetryAfter(error: unknown): string;
5
7
  export declare function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined;
6
8
  export declare function getHeadersFromError(error: unknown): HeadersLike;
@@ -5,11 +5,17 @@ import { AssistantMessageEventStream } from "./event-stream.js";
5
5
  * classifiers treat it as a transient (retryable) stop without bespoke rules. */
6
6
  export declare const THINKING_LOOP_ERROR_MARKER = "Thinking loop detected";
7
7
  /**
8
- * True when resolved compatibility policy enables semantic loop heuristics for
9
- * this model. Exact suffix-cycle detection applies to every enabled model
10
- * independently of this predicate.
8
+ * How much of the loop guard runs on a stream: `off` none, `exact` only
9
+ * exact suffix cycles, `semantic` exact cycles plus the near-duplicate and
10
+ * progress-lexicon paragraph heuristics.
11
11
  */
12
- export declare function isLoopGuardedModel(model: Model<Api>, options?: StreamOptions): boolean;
12
+ export type LoopGuardLevel = "off" | "exact" | "semantic";
13
+ /**
14
+ * Loop-guard level for a model stream. `PI_NO_THINKING_LOOP_GUARD=1` and
15
+ * `loopGuard.enabled: false` turn it off; otherwise the model line's
16
+ * `thinking-loop-guard` catalog axis decides, and unassigned lines get `exact`.
17
+ */
18
+ export declare function loopGuardLevel(model: Model<Api>, options?: StreamOptions): LoopGuardLevel;
13
19
  /**
14
20
  * Stateful detector fed the streamed thinking deltas. `push` returns a
15
21
  * human-readable reason the first time a loop shape is recognized; the caller
@@ -75,11 +81,10 @@ export declare class GeminiHeaderRunDetector {
75
81
  */
76
82
  export declare function guardThinkingLoopStream(inner: AssistantMessageEventStream, model: Model<Api>, controller: AbortController, options?: StreamOptions): AssistantMessageEventStream;
77
83
  /**
78
- * Apply the loop guard around a provider dispatch. Unless explicitly disabled,
79
- * every model gets exact suffix-cycle detection; Gemini, DeepSeek, and Grok also
80
- * get the semantic heuristics selected by {@link isLoopGuardedModel}. The guard
81
- * injects an abort signal into the provider call so a detected loop tears down
82
- * the upstream, then wraps the returned stream. Bounding result-path re-samples
84
+ * Apply the loop guard around a provider dispatch at the model's
85
+ * {@link loopGuardLevel}; `off` dispatches unwrapped. The guard injects an
86
+ * abort signal into the provider call so a detected loop tears down the
87
+ * upstream, then wraps the returned stream. Bounding result-path re-samples
83
88
  * lives in the result-awaiting caller.
84
89
  */
85
90
  export declare function withThinkingLoopGuard<O extends {
@@ -34,6 +34,11 @@ export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem ex
34
34
  type?: unknown;
35
35
  status?: unknown;
36
36
  }>(items: TItem[]): TItem[];
37
+ /** Remove Codex-rejected backend metadata; preserve clean-item identity by default, or copy while keeping/dropping id. */
38
+ export declare function stripOpenAIResponsesInternalMetadataForReplay<TItem extends {
39
+ id?: unknown;
40
+ internal_chat_message_metadata_passthrough?: unknown;
41
+ }>(item: TItem, mode?: "preserve" | "copy" | "drop-id"): TItem;
37
42
  /** Clamp replayed images without changing item identities or other native replay fields. */
38
43
  export declare function clampOpenAIResponsesImageDetailForReplay<TItem extends {
39
44
  type?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.14.1",
3
+ "version": "18.15.0",
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.14.1",
159
- "@oh-my-pi/pi-catalog": "18.14.1",
160
- "@oh-my-pi/pi-natives": "18.14.1",
161
- "@oh-my-pi/pi-utils": "18.14.1",
162
- "@oh-my-pi/pi-wire": "18.14.1"
158
+ "@oh-my-pi/omptype": "18.15.0",
159
+ "@oh-my-pi/pi-catalog": "18.15.0",
160
+ "@oh-my-pi/pi-natives": "18.15.0",
161
+ "@oh-my-pi/pi-utils": "18.15.0",
162
+ "@oh-my-pi/pi-wire": "18.15.0"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -367,10 +367,7 @@ export class OAuthRefresher {
367
367
  this.#armShutdownDrain();
368
368
  const settled = () => {
369
369
  this.#pendingRefreshes.delete(pending);
370
- if (this.#pendingRefreshes.size === 0 && !this.#shuttingDown) {
371
- this.#cancelRefreshCleanup?.();
372
- this.#cancelRefreshCleanup = undefined;
373
- }
370
+ if (this.#pendingRefreshes.size === 0 && !this.#shuttingDown) this.#disarmShutdownDrain();
374
371
  };
375
372
  void pending.then(settled, settled);
376
373
  return pending;
@@ -389,12 +386,41 @@ export class OAuthRefresher {
389
386
  });
390
387
  }
391
388
 
389
+ #disarmShutdownDrain(): void {
390
+ this.#cancelRefreshCleanup?.();
391
+ this.#cancelRefreshCleanup = undefined;
392
+ }
393
+
392
394
  async #drainRefreshes(): Promise<void> {
393
395
  this.#shuttingDown = true;
394
396
  // Work tracked while draining (a lease hand-back a returning caller left behind) is waited for too.
395
397
  while (this.#pendingRefreshes.size > 0) await Promise.allSettled(this.#pendingRefreshes);
396
398
  }
397
399
 
400
+ /**
401
+ * Stop admitting refreshes, then run `closeStore` once tracked work settles:
402
+ * a token exchange already under way, its save, a lease hand-back. Runs it at
403
+ * once when nothing is in flight. Closing the store under that work would
404
+ * lose a refresh token the provider already rotated. Called by
405
+ * {@link AuthStorage.close} and {@link AuthStorage.replaceStore}.
406
+ */
407
+ close(closeStore: () => void): void {
408
+ this.#shuttingDown = true;
409
+ if (this.#pendingRefreshes.size === 0) {
410
+ this.#disarmShutdownDrain();
411
+ closeStore();
412
+ return;
413
+ }
414
+ void this.#drainRefreshes().then(() => {
415
+ this.#disarmShutdownDrain();
416
+ try {
417
+ closeStore();
418
+ } catch (error) {
419
+ logger.warn("Credential store close after OAuth refresh drain failed", { error: String(error) });
420
+ }
421
+ });
422
+ }
423
+
398
424
  async #refreshStored<T extends OAuthCredential = OAuthCredential>(
399
425
  provider: string,
400
426
  options: StoredOAuthRefreshOptions<T>,
@@ -884,7 +910,12 @@ export class OAuthRefresher {
884
910
  return "disabled";
885
911
  }
886
912
 
887
- async refresh(
913
+ /**
914
+ * Refresh `credential` unless it is still fresh. Shutdown waits for the
915
+ * refresh, and for the caller's synchronous save of its result, before the
916
+ * store closes.
917
+ */
918
+ refresh(
888
919
  provider: Provider,
889
920
  credential: OAuthCredential,
890
921
  credentialId: number | undefined,
@@ -892,13 +923,14 @@ export class OAuthRefresher {
892
923
  reason?: OAuthRefreshReason,
893
924
  rejectedAccess?: string,
894
925
  ): Promise<OAuthCredentials> {
926
+ if (this.#shuttingDown) return Promise.reject(new AIError.AbortError("OAuth refresh stopped during shutdown"));
895
927
  const authRecovery = reason === "auth-recovery";
896
928
  if (authRecovery && credentialId !== undefined && !this.#oauthCredentialRefreshInFlight.has(credentialId)) {
897
929
  const recent = this.#recentMint(credentialId);
898
930
  // A token the provider has just rejected is not a usable recent mint.
899
- if (recent && recent.credential.access !== rejectedAccess) return recent.credential;
931
+ if (recent && recent.credential.access !== rejectedAccess) return Promise.resolve(recent.credential);
900
932
  }
901
- return this.#refreshSingleFlight(provider, credential, credentialId, signal, reason, rejectedAccess);
933
+ return this.#track(this.#refreshSingleFlight(provider, credential, credentialId, signal, reason, rejectedAccess));
902
934
  }
903
935
 
904
936
  /** Refresh without recent-mint reuse; still shares the per-credential in-flight request. */
@@ -947,7 +979,8 @@ export class OAuthRefresher {
947
979
  .finally(() => {
948
980
  this.#oauthCredentialRefreshInFlight.delete(credentialId);
949
981
  });
950
- this.#oauthCredentialRefreshInFlight.set(credentialId, promise);
982
+ // Tracked apart from the caller: a refresh its caller abandoned still has to finish before the store closes.
983
+ this.#oauthCredentialRefreshInFlight.set(credentialId, this.#track(promise));
951
984
  return raceSignal(promise, signal, "credential refresh aborted");
952
985
  }
953
986
 
@@ -973,7 +1006,9 @@ export class OAuthRefresher {
973
1006
  // `refreshStored` owns the caller budget and the exchange backstop.
974
1007
  refresh: (current, refreshSignal) =>
975
1008
  this.#requestOAuthCredentialRefresh(provider, current, credentialId, refreshSignal, reason, undefined),
976
- isDefinitiveFailure: error => AIError.isDefinitiveOAuthFailure(String(error)),
1009
+ // A usage probe is advisory: leave a dead grant for an interactive request to disable.
1010
+ isDefinitiveFailure:
1011
+ reason === "usage-probe" ? undefined : error => AIError.isDefinitiveOAuthFailure(String(error)),
977
1012
  disabledCause: error => `oauth refresh failed: ${String(error)}`,
978
1013
  });
979
1014
  if (result.credential) {
@@ -1088,6 +1123,7 @@ export class OAuthRefresher {
1088
1123
  signal?: AbortSignal,
1089
1124
  options?: OAuthRefreshByIdOptions,
1090
1125
  ): Promise<AuthCredentialSnapshotEntry> {
1126
+ if (this.#shuttingDown) throw new AIError.AbortError("OAuth refresh stopped during shutdown");
1091
1127
  const existing = this.#oauthRefreshInFlight.get(id);
1092
1128
  if (existing) {
1093
1129
  const joined = await raceSignal(existing, signal, "credential refresh aborted");
@@ -1122,7 +1158,8 @@ export class OAuthRefresher {
1122
1158
  this.#oauthRefreshInFlight.delete(id);
1123
1159
  }
1124
1160
  })();
1125
- this.#oauthRefreshInFlight.set(id, promise);
1161
+ // Saves by id itself; tracked so the store stays open until that save lands.
1162
+ this.#oauthRefreshInFlight.set(id, this.#track(promise));
1126
1163
  return raceSignal(promise, signal, "credential refresh aborted");
1127
1164
  }
1128
1165
 
@@ -1164,7 +1201,8 @@ export class OAuthRefresher {
1164
1201
  // A definitively-dead grant tears the row down here, where the
1165
1202
  // attempted credential is known. CAS on the persisted credential so a
1166
1203
  // peer/login rotation in flight leaves the freshly-rotated row intact.
1167
- if (AIError.isDefinitiveOAuthFailure(String(error))) {
1204
+ // A usage probe is advisory: leave a dead grant for an interactive request to disable.
1205
+ if (reason !== "usage-probe" && AIError.isDefinitiveOAuthFailure(String(error))) {
1168
1206
  // CAS-loss (false) means a peer/login rotated the row mid-refresh, so
1169
1207
  // our #data copy is stale — reload so the next caller serves the
1170
1208
  // freshly-rotated credential rather than the dead token we attempted.