@oh-my-pi/pi-ai 18.13.2 → 18.14.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 (73) hide show
  1. package/CHANGELOG.md +41 -36
  2. package/dist/types/auth/abort.d.ts +3 -1
  3. package/dist/types/auth/cascade.d.ts +2 -0
  4. package/dist/types/auth/select.d.ts +2 -0
  5. package/dist/types/auth/store.d.ts +1 -1
  6. package/dist/types/auth/types.d.ts +9 -2
  7. package/dist/types/auth/usage.d.ts +1 -0
  8. package/dist/types/auth-broker/client.d.ts +3 -2
  9. package/dist/types/auth-broker/remote-store.d.ts +3 -3
  10. package/dist/types/auth-gateway/dispatch.d.ts +17 -1
  11. package/dist/types/error/abort.d.ts +5 -0
  12. package/dist/types/error/finalize.d.ts +5 -4
  13. package/dist/types/index.d.ts +3 -0
  14. package/dist/types/providers/anthropic-headers.d.ts +84 -0
  15. package/dist/types/providers/anthropic-http-options.d.ts +15 -0
  16. package/dist/types/providers/anthropic.d.ts +1 -60
  17. package/dist/types/providers/cursor.d.ts +2 -1
  18. package/dist/types/providers/openai-codex-responses.d.ts +110 -37
  19. package/dist/types/providers/openai-codex-state.d.ts +43 -0
  20. package/dist/types/providers/openai-completions.d.ts +16 -0
  21. package/dist/types/providers/openai-responses-state.d.ts +48 -0
  22. package/dist/types/providers/openai-responses.d.ts +2 -34
  23. package/dist/types/providers/pi-native-server.d.ts +2 -1
  24. package/dist/types/providers/register-builtins.d.ts +3 -3
  25. package/dist/types/providers/transform-messages.d.ts +9 -0
  26. package/dist/types/utils/abort.d.ts +4 -1
  27. package/package.json +8 -8
  28. package/src/auth/abort.ts +7 -2
  29. package/src/auth/cascade.ts +7 -0
  30. package/src/auth/rank.ts +6 -4
  31. package/src/auth/refresh.ts +15 -10
  32. package/src/auth/select.ts +98 -52
  33. package/src/auth/store.ts +1 -1
  34. package/src/auth/types.ts +9 -2
  35. package/src/auth/usage.ts +14 -4
  36. package/src/auth-broker/client.ts +7 -4
  37. package/src/auth-broker/remote-store.ts +47 -26
  38. package/src/auth-broker/server.ts +4 -1
  39. package/src/auth-gateway/dispatch.ts +22 -2
  40. package/src/auth-gateway/server.ts +18 -18
  41. package/src/error/abort.ts +12 -0
  42. package/src/error/finalize.ts +10 -5
  43. package/src/index.ts +3 -0
  44. package/src/judgment/native.ts +10 -6
  45. package/src/judgment/text-judge-retry.md +1 -1
  46. package/src/judgment/text-judge-state.md +1 -1
  47. package/src/judgment/text-judge.md +0 -3
  48. package/src/judgment/text.ts +1 -9
  49. package/src/provider-details.ts +1 -1
  50. package/src/provider-session-state.ts +1 -1
  51. package/src/providers/anthropic-headers.ts +412 -0
  52. package/src/providers/anthropic-http-options.ts +148 -0
  53. package/src/providers/anthropic.ts +25 -549
  54. package/src/providers/connection-prewarm.ts +1 -1
  55. package/src/providers/cowork-fetch.ts +0 -3
  56. package/src/providers/cursor.ts +41 -2
  57. package/src/providers/factory-droid/droid-identity.md +1 -1
  58. package/src/providers/factory-droid.ts +2 -1
  59. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -1
  60. package/src/providers/openai-codex-responses.ts +23 -186
  61. package/src/providers/openai-codex-state.ts +194 -0
  62. package/src/providers/openai-completions.ts +40 -11
  63. package/src/providers/openai-responses-state.ts +100 -0
  64. package/src/providers/openai-responses.ts +7 -93
  65. package/src/providers/openai-shared.ts +6 -1
  66. package/src/providers/pi-native-client.ts +60 -9
  67. package/src/providers/pi-native-server.ts +35 -4
  68. package/src/providers/register-builtins.ts +68 -52
  69. package/src/providers/transform-messages.ts +27 -7
  70. package/src/registry/engine/api-key.ts +3 -1
  71. package/src/utils/abort.ts +11 -2
  72. package/src/utils/anthropic-auth.ts +5 -1
  73. package/THIRD-PARTY-NOTICES.txt +0 -22901
@@ -1,8 +1,11 @@
1
- import type { CodexCompactionContext, CodexCompactionRequestContext, Context, Model, ProviderSessionState, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types.js";
2
- import { type CodexLiteShapedBody, type CodexReasoningContext, type InputItem, type RequestBody } from "./openai-codex/request-transformer.js";
1
+ import type { CodexCompactionRequestContext, Context, Model, ProviderSessionState, RawSseEvent, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types.js";
2
+ import { type HeadersLike } from "../utils/retry-after.js";
3
+ import { type CodexLiteShapedBody, type CodexReasoningContext, type InputItem, type ReasoningConfig, type RequestBody } from "./openai-codex/request-transformer.js";
4
+ import { type CodexAcceptedSteer, type CodexSteerAck } from "./openai-codex/live-steering.js";
3
5
  export { createOpenAICodexCompactionRequestContext } from "./openai-codex-compaction.js";
4
6
  export { setCodexAttestationProvider } from "./openai-codex-attestation.js";
5
7
  export type { CodexAttestationProvider } from "./openai-codex-attestation.js";
8
+ import { type OpenAIEffortControlState } from "./openai-configuration-update.js";
6
9
  import type { ResponseInput } from "./openai-responses-wire.js";
7
10
  export interface OpenAICodexResponsesOptions extends StreamOptions {
8
11
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
@@ -76,13 +79,7 @@ export interface OpenAICodexCompatibilityMetadata {
76
79
  clientMetadata: Record<string, string>;
77
80
  headers: Record<string, string>;
78
81
  }
79
- /** Live Codex session state to preserve after a successful history rewrite. */
80
- export interface OpenAICodexCompactionResetOptions {
81
- providerSessionState?: Map<string, ProviderSessionState>;
82
- sessionId?: string;
83
- compaction: CodexCompactionContext;
84
- }
85
- type CodexTransport = "sse" | "websocket";
82
+ export type CodexTransport = "sse" | "websocket";
86
83
  /** Shape of the Codex request sent on the latest provider turn. */
87
84
  export interface OpenAICodexTurnRequestDiagnostics {
88
85
  transport: "sse" | "websocket";
@@ -132,18 +129,65 @@ export interface OpenAICodexWebSocketDebugStats {
132
129
  lastPreviousResponseId?: string;
133
130
  lastTurn?: OpenAICodexTurnDiagnostics;
134
131
  }
132
+ /**
133
+ * Per-session transport state shared by both transports: WebSocket turn
134
+ * chaining, models-etag headers, connection pooling, and debug stats. The name
135
+ * is historical — SSE-only sessions use it too.
136
+ */
137
+ export type CodexWebSocketSessionState = {
138
+ sessionId: string;
139
+ disableWebsocket: boolean;
140
+ lastRequest?: RequestBody;
141
+ /** Last completed response; an in-progress response cannot replace the retry baseline. */
142
+ lastResponseId?: string;
143
+ lastResponseItems?: InputItem[];
144
+ canAppend: boolean;
145
+ modelsEtag?: string;
146
+ connection?: CodexWebSocketConnection;
147
+ /**
148
+ * Steering the server accepted on `connection` for `lastResponseId`. The
149
+ * next request reads the server's automatic successor or sends only the
150
+ * pending tool output; see {@link planSteeredRequest}.
151
+ */
152
+ acceptedSteering?: {
153
+ connection: CodexWebSocketConnection;
154
+ steers: CodexAcceptedSteer[];
155
+ };
156
+ /** The server refused steering for this session (native turn lane); later responses are not steered. */
157
+ steeringUnsupported?: boolean;
158
+ lastTransport?: CodexTransport;
159
+ fallbackCount: number;
160
+ lastFallbackAt?: number;
161
+ prewarmed: boolean;
162
+ stats: OpenAICodexWebSocketDebugStats;
163
+ };
164
+ interface CodexTurnStateCell {
165
+ value?: string;
166
+ }
167
+ export interface CodexProviderSessionState extends ProviderSessionState {
168
+ webSocketSessions: Map<string, CodexWebSocketSessionState>;
169
+ webSocketPublicToPrivate: Map<string, string>;
170
+ metadataSessions: Map<string, CodexMetadataSessionState>;
171
+ /** `configuration_update` effort baselines, keyed by model + session. */
172
+ effortControls: Map<string, OpenAIEffortControlState<ReasoningConfig["effort"]>>;
173
+ }
135
174
  /** Request classification encoded in Codex turn metadata. */
136
175
  export type OpenAICodexRequestKind = "turn" | "prewarm" | "compaction";
176
+ interface CodexMetadataSessionState {
177
+ sessionId: string;
178
+ threadId: string;
179
+ windowId: string;
180
+ turnId?: string;
181
+ turnStartedAtUnixMs?: number;
182
+ compactionOperationId?: string;
183
+ reuseTurnForNextRequest?: boolean;
184
+ turnStates: Map<string, CodexTurnStateCell>;
185
+ }
137
186
  /**
138
187
  * Synthesize Codex request identity for raw provider routes such as remote
139
188
  * compaction while reusing the live session's thread, window, and turn.
140
189
  */
141
190
  export declare function createOpenAICodexCompatibilityMetadata(options: OpenAICodexCompatibilityMetadataOptions): OpenAICodexCompatibilityMetadata;
142
- /**
143
- * Invalidate Codex history-dependent transport state after compaction while
144
- * retaining the session identity and live connection.
145
- */
146
- export declare function resetOpenAICodexHistoryAfterCompaction(options: OpenAICodexCompactionResetOptions): void;
147
191
  /** @internal Exported for tests. */
148
192
  export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined, tools?: Tool[], model?: Model<"openai-codex-responses">): string | Record<string, unknown> | undefined;
149
193
  /** Serialize normal Codex turns and V2 compaction with the same cacheable prefix. */
@@ -155,29 +199,57 @@ export declare function buildTransformedCodexRequestBody(model: Model<"openai-co
155
199
  export declare function openCodexCompactionEventStream(model: Model<"openai-codex-responses">, body: OpenAICodexCompactionBody, options: OpenAICodexCompactionStreamOptions): Promise<AsyncGenerator<Record<string, unknown>>>;
156
200
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
157
201
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite" | "serviceTier">): Promise<void>;
158
- export interface OpenAICodexTransportDetails {
159
- websocketPreferred: boolean;
160
- lastTransport?: CodexTransport;
161
- websocketDisabled: boolean;
162
- websocketConnected: boolean;
163
- fallbackCount: number;
164
- canAppend: boolean;
165
- prewarmed: boolean;
166
- hasSessionState: boolean;
167
- hasTurnState: boolean;
168
- lastFallbackAt?: number;
202
+ interface CodexWebSocketRequestTimeouts {
203
+ idleTimeoutMs?: number;
204
+ firstEventTimeoutMs?: number;
205
+ }
206
+ /** What {@link CodexWebSocketConnection.streamRequest} puts on the wire before reading a response. */
207
+ type CodexWebSocketExchange = {
208
+ send: Record<string, unknown>;
209
+ }
210
+ /** Read the successor the server creates from accepted steering; nothing is sent. */
211
+ | {
212
+ attachSteerIds: readonly string[];
213
+ };
214
+ interface CodexWebSocketConnectionOptions {
215
+ onHandshakeHeaders?: (headers: Headers) => void;
216
+ proxy?: string;
217
+ }
218
+ declare class CodexWebSocketConnection {
219
+ #private;
220
+ constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions);
221
+ isOpen(): boolean;
222
+ /** True while a handshake (possibly started by another caller) is still in flight. */
223
+ isConnecting(): boolean;
224
+ /** True while a handshake or a request stream is in flight on this socket. */
225
+ isBusy(): boolean;
226
+ /** True while a request stream owns this socket. */
227
+ hasActiveRequest(): boolean;
228
+ /**
229
+ * Stricter variant of {@link isOpen} for the connection-pool reuse gate.
230
+ * Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
231
+ *
232
+ * Bun's `WebSocket` does not always surface server-side eviction (no
233
+ * `onclose`, no `onerror`), so a socket can sit in readyState OPEN long
234
+ * after the upstream has dropped it. Reusing such a socket sends the next
235
+ * `response.create` into a half-open write buffer and parks the reader
236
+ * until the first-event / idle timeout fires (issue #1450). Forcing a
237
+ * reconnect on any suspect socket trades a sub-second handshake for a
238
+ * 60–300 s stall.
239
+ */
240
+ isHealthyForReuse(): boolean;
241
+ matchesAuth(headers: Record<string, string>): boolean;
242
+ close(reason?: string): void;
243
+ connect(signal?: AbortSignal): Promise<void>;
244
+ streamRequest(exchange: CodexWebSocketExchange, timeouts: CodexWebSocketRequestTimeouts, signal?: AbortSignal, onSseEvent?: (event: RawSseEvent) => void, onOwned?: () => void): AsyncGenerator<Record<string, unknown>>;
245
+ /**
246
+ * Submit `response.steer` for `previousResponseId`, which must be streaming
247
+ * on this socket. Resolves on the server's acknowledgement.
248
+ */
249
+ steer(previousResponseId: string, input: InputItem[]): Promise<CodexSteerAck>;
250
+ /** True when the server failed to commit any of the accepted steering `ids`. */
251
+ hasFailedSteer(ids: readonly string[]): boolean;
169
252
  }
170
- export declare function getOpenAICodexWebSocketDebugStats(model: Model<"openai-codex-responses">, options?: {
171
- sessionId?: string;
172
- baseUrl?: string;
173
- providerSessionState?: Map<string, ProviderSessionState>;
174
- }): OpenAICodexWebSocketDebugStats | undefined;
175
- export declare function getOpenAICodexTransportDetails(model: Model<"openai-codex-responses">, options?: {
176
- sessionId?: string;
177
- baseUrl?: string;
178
- preferWebsockets?: boolean;
179
- providerSessionState?: Map<string, ProviderSessionState>;
180
- }): OpenAICodexTransportDetails;
181
253
  /** Resolve a Codex Responses endpoint exactly as the chat and compaction transports do. */
182
254
  export declare function resolveCodexResponsesUrl(baseUrl: string | undefined): string;
183
255
  declare function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput;
@@ -210,7 +282,8 @@ export declare class CodexWebSocketTransportError extends Error {
210
282
  declare class CodexProviderStreamError extends Error {
211
283
  readonly retryable: boolean;
212
284
  readonly code?: string;
213
- constructor(message: string, retryable: boolean, code?: string);
285
+ readonly headers?: HeadersLike;
286
+ constructor(message: string, retryable: boolean, code?: string, headers?: HeadersLike);
214
287
  }
215
288
  export declare function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean;
216
289
  export declare function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError;
@@ -0,0 +1,43 @@
1
+ import type { CodexCompactionContext, Model, ProviderSessionState } from "../types.js";
2
+ import type { CodexProviderSessionState, CodexTransport, CodexWebSocketSessionState, OpenAICodexWebSocketDebugStats } from "./openai-codex-responses.js";
3
+ /** Live Codex session state to preserve after a successful history rewrite. */
4
+ export interface OpenAICodexCompactionResetOptions {
5
+ providerSessionState?: Map<string, ProviderSessionState>;
6
+ sessionId?: string;
7
+ compaction: CodexCompactionContext;
8
+ }
9
+ /**
10
+ * Invalidate Codex history-dependent transport state after compaction while
11
+ * retaining the session identity and live connection.
12
+ */
13
+ export declare function resetOpenAICodexHistoryAfterCompaction(options: OpenAICodexCompactionResetOptions): void;
14
+ /** Create isolated Codex transport state for a session or compaction request. */
15
+ export declare function createCodexProviderSessionState(): CodexProviderSessionState;
16
+ /** Return or initialize the Codex state shared by a session's streaming requests. */
17
+ export declare function getCodexProviderSessionState(providerSessionState: Map<string, ProviderSessionState> | undefined): CodexProviderSessionState | undefined;
18
+ /** Forget append baselines after a history rewrite without closing the live connection. */
19
+ export declare function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void;
20
+ /** Current Codex transport selection and connection status for host diagnostics. */
21
+ export interface OpenAICodexTransportDetails {
22
+ websocketPreferred: boolean;
23
+ lastTransport?: CodexTransport;
24
+ websocketDisabled: boolean;
25
+ websocketConnected: boolean;
26
+ fallbackCount: number;
27
+ canAppend: boolean;
28
+ prewarmed: boolean;
29
+ hasSessionState: boolean;
30
+ hasTurnState: boolean;
31
+ lastFallbackAt?: number;
32
+ }
33
+ export declare function getOpenAICodexWebSocketDebugStats(model: Model<"openai-codex-responses">, options?: {
34
+ sessionId?: string;
35
+ baseUrl?: string;
36
+ providerSessionState?: Map<string, ProviderSessionState>;
37
+ }): OpenAICodexWebSocketDebugStats | undefined;
38
+ export declare function getOpenAICodexTransportDetails(model: Model<"openai-codex-responses">, options?: {
39
+ sessionId?: string;
40
+ baseUrl?: string;
41
+ preferWebsockets?: boolean;
42
+ providerSessionState?: Map<string, ProviderSessionState>;
43
+ }): OpenAICodexTransportDetails;
@@ -3,6 +3,11 @@ 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
+ /**
7
+ * Whether a thinking block's signature is a raw LiteLLM (Anthropic) one, not a
8
+ * reasoning field name or the Mistral content-parts marker.
9
+ */
10
+ export declare function isLiteLLMThinkingSignature(signature: string | undefined): signature is string;
6
11
  /**
7
12
  * Identify "real progress" stream chunks vs. keepalives, role-only preambles,
8
13
  * and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
@@ -43,4 +48,15 @@ export interface OpenAICompletionsOptions extends StreamOptions {
43
48
  */
44
49
  export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
45
50
  export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined, timestamp?: number, cachedTokensHeader?: number): AssistantMessage["usage"];
51
+ /**
52
+ * Whether every chat-completions request to `model` drops the thinking text of
53
+ * its own prior turns: neither the base compat nor its `whenThinking` view
54
+ * sends it. Blocks with a raw LiteLLM signature ({@link isLiteLLMThinkingSignature})
55
+ * still replay in `thinking_blocks`.
56
+ */
57
+ export declare function completionsDropsOwnThinking(model: {
58
+ api?: string;
59
+ reasoning?: boolean;
60
+ compat?: Model["compat"];
61
+ } | null | undefined): boolean;
46
62
  export declare function convertMessages(model: Model<"openai-completions">, context: Context, compat: ResolvedOpenAICompat): ChatCompletionMessageParam[];
@@ -0,0 +1,48 @@
1
+ import type { Model, ProviderSessionState } from "../types.js";
2
+ import type { OpenAIResponsesChainState, OpenAIResponsesOptions } from "./openai-responses.js";
3
+ /** Prefix shared by Responses request state and credential-reset hooks. */
4
+ export declare const OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX = "openai-responses:";
5
+ /**
6
+ * Whether the next request for `model` replays native Responses history items
7
+ * (encrypted reasoning, item ids), matching request construction. A session
8
+ * is cold, rebuilding turns from message content, from the first request on a
9
+ * connection-bound host (`compat.connectionBoundNativeHistory`, GitHub Copilot)
10
+ * and after `close()` on any host, until a response succeeds. Without session
11
+ * state every request replays.
12
+ */
13
+ export declare function openAIResponsesReplaysNativeHistory(model: Pick<Model, "api" | "provider" | "compat"> | null | undefined, providerSessionState: Map<string, ProviderSessionState> | undefined): boolean;
14
+ /**
15
+ * Set the process-wide storage default per provider id, used when a request
16
+ * leaves `storeResponses` unset. Hosts call this from their settings so every
17
+ * request follows the setting, including direct `streamSimple`/`completeSimple`
18
+ * side calls that do not pass per-request options. `PI_MUSE_STORE_RESPONSES`
19
+ * still overrides it.
20
+ */
21
+ export declare function configureProviderStoreResponses(byProvider: Readonly<Record<string, boolean>> | undefined): void;
22
+ /**
23
+ * Whether this request stores its result server-side. Only hosts whose rule
24
+ * sets `store-responses` (they finish runs after a client disconnect, so a
25
+ * dropped stream can be resumed) are eligible, and storage is opt-in:
26
+ * `options.storeResponses`, then `PI_MUSE_STORE_RESPONSES`, then the host
27
+ * default ({@link configureProviderStoreResponses}), else off. Privacy: stored
28
+ * runs retain prompts and outputs on the provider. Off also disables resume and
29
+ * the `previous_response_id` chaining that needs storage.
30
+ */
31
+ export declare function storesResponsesServerSide(model: Model<"openai-responses">, options: OpenAIResponsesOptions | undefined): boolean;
32
+ /** Forget server-side append baselines while preserving the chain's failure history. */
33
+ export declare function resetOpenAIResponsesChainState(state: OpenAIResponsesChainState): void;
34
+ /**
35
+ * Drop the account-bound half of every retained `openai-responses` record in
36
+ * `states`: the stateful `previous_response_id` chain baselines.
37
+ *
38
+ * Chaining stores the turn server-side under the account that created it, so a
39
+ * baseline minted by one credential is dead weight the moment the session is
40
+ * switched to a sibling account — the next delta request answers
41
+ * `Previous response not found` and burns a turn re-learning that. Everything
42
+ * else this record holds describes the *deployment*, not the account
43
+ * (strict-tools demotion, reasoning-effort fallback, native-history-replay
44
+ * warmup, the chaining circuit breaker), and is deliberately preserved:
45
+ * re-learning an endpoint's limits on every credential switch is the cost this
46
+ * state exists to avoid.
47
+ */
48
+ export declare function resetOpenAIResponsesAccountScopedState(states: Map<string, ProviderSessionState>): void;
@@ -81,7 +81,7 @@ export declare function pollOpenAIResponsesResultForCompletion(args: {
81
81
  /** Sleep between polls; defaults to a real wait honoring `signal`. */
82
82
  wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
83
83
  }): Promise<Record<string, unknown> | undefined>;
84
- interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
84
+ export interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
85
85
  /**
86
86
  * Replay native history items. Starts false only on connection-bound hosts;
87
87
  * `close()` clears it so the next request rebuilds history from message
@@ -97,7 +97,7 @@ interface OpenAIResponsesProviderSessionState extends ProviderSessionState, Open
97
97
  }
98
98
  /** Wire efforts a `configuration_update` can carry: every real tier, never `none`/null. */
99
99
  type ResponsesStableEffort = Exclude<ReasoningEffort, "none" | null>;
100
- interface OpenAIResponsesChainState {
100
+ export interface OpenAIResponsesChainState {
101
101
  sessionId: string;
102
102
  /**
103
103
  * Wire params of the last successful turn; never carries
@@ -114,38 +114,6 @@ 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;
126
- /**
127
- * Set the process-wide storage default per provider id, used when a request
128
- * leaves `storeResponses` unset. Hosts call this from their settings so every
129
- * request follows the setting, including direct `streamSimple`/`completeSimple`
130
- * side calls that do not pass per-request options. `PI_MUSE_STORE_RESPONSES`
131
- * still overrides it.
132
- */
133
- export declare function configureProviderStoreResponses(byProvider: Readonly<Record<string, boolean>> | undefined): void;
134
- /**
135
- * Drop the account-bound half of every retained `openai-responses` record in
136
- * `states`: the stateful `previous_response_id` chain baselines.
137
- *
138
- * Chaining stores the turn server-side under the account that created it, so a
139
- * baseline minted by one credential is dead weight the moment the session is
140
- * switched to a sibling account — the next delta request answers
141
- * `Previous response not found` and burns a turn re-learning that. Everything
142
- * else this record holds describes the *deployment*, not the account
143
- * (strict-tools demotion, reasoning-effort fallback, native-history-replay
144
- * warmup, the chaining circuit breaker), and is deliberately preserved:
145
- * re-learning an endpoint's limits on every credential switch is the cost this
146
- * state exists to avoid.
147
- */
148
- export declare function resetOpenAIResponsesAccountScopedState(states: Map<string, ProviderSessionState>): void;
149
117
  type OpenRouterAnthropicCacheControl = {
150
118
  type: "ephemeral";
151
119
  ttl?: "1h";
@@ -21,7 +21,8 @@
21
21
  * Endpoint contract:
22
22
  * POST /v1/pi/stream
23
23
  * body: { modelId, context, options?, stream? } // `stream` defaults to true
24
- * 200 SSE: stream of `AssistantMessageEvent` (terminated by `data: [DONE]`)
24
+ * 200 SSE: stream of `AssistantMessageEvent` (terminated by `data: [DONE]`),
25
+ * interleaved with `: keepalive` comment frames
25
26
  * 200 JSON (stream=false): { message: AssistantMessage }
26
27
  * 4xx/5xx: { error: { type, message } }
27
28
  */
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Built-in provider stream dispatch with shared error, cancellation, and timeout handling.
2
+ * First-use provider loading with shared stream error, cancellation, and timeout handling.
3
3
  */
4
4
  import type { Context, Model, OptionsForApi } from "../types.js";
5
5
  import { AssistantMessageEventStream as EventStreamImpl } from "../utils/event-stream.js";
6
- import * as BedrockProvider from "./amazon-bedrock.js";
7
- import * as CursorProvider from "./cursor.js";
6
+ import type * as BedrockProvider from "./amazon-bedrock.js";
7
+ import type * as CursorProvider from "./cursor.js";
8
8
  /** Install a host-supplied Bedrock transport in place of the built-in provider. */
9
9
  export declare function setBedrockProviderModule(module: Pick<typeof BedrockProvider, "streamBedrock">): void;
10
10
  /** Install a host-supplied Cursor transport in place of the built-in provider. */
@@ -1,3 +1,4 @@
1
+ import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
1
2
  import type { Api, AssistantMessage, Message, Model } from "../types.js";
2
3
  /**
3
4
  * Maximum tool-call id length the strictest replay provider accepts.
@@ -54,6 +55,14 @@ export declare function isSurvivingAssistant(msg: AssistantMessage, model?: Mode
54
55
  export declare function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages">;
55
56
  /** Whether `turn` came from `model` itself: its own turns replay their native reasoning unchanged. */
56
57
  export declare function isSameModelTurn(turn: Pick<AssistantMessage, "provider" | "api" | "model">, model: Pick<Model, "provider" | "api" | "id">): boolean;
58
+ /**
59
+ * Whether chat completions (`convertMessages`) writes the model's own past
60
+ * thinking under `compat`: as Mistral content parts, demoted text, or a
61
+ * reasoning field. Otherwise it drops it.
62
+ */
63
+ export declare function completionsSendsOwnThinking(model: {
64
+ reasoning?: boolean;
65
+ }, compat: Pick<ResolvedOpenAICompat, "mistralReasoningContentParts" | "requiresThinkingAsText" | "requiresReasoningContentForToolCalls" | "thinkingFormat" | "replayReasoningContent">): boolean;
57
66
  /**
58
67
  * Normalize tool call ID for cross-provider compatibility.
59
68
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
@@ -3,6 +3,8 @@ export interface AbortSourceTracker {
3
3
  requestSignal: AbortSignal;
4
4
  abortLocally(reason: Error): Error;
5
5
  getLocalAbortReason(): Error | undefined;
6
+ /** The reason the caller passed to `abort()`, read from the caller's own signal, never the racy merged one. */
7
+ getCallerAbortReason(): string | undefined;
6
8
  wasCallerAbort(): boolean;
7
9
  }
8
10
  /**
@@ -20,6 +22,7 @@ export declare function createAbortSourceTracker(callerSignal?: AbortSignal): Ab
20
22
  /**
21
23
  * Race a shared promise against a caller's AbortSignal without coupling the
22
24
  * underlying work to that signal. The shared promise keeps running (and caches
23
- * its result) even when an individual caller bails out.
25
+ * its result) even when an individual caller bails out, and stays observed so
26
+ * its later rejection is never unhandled.
24
27
  */
25
28
  export declare function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.13.2",
3
+ "version": "18.14.0",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,7 +33,6 @@
33
33
  "README.md",
34
34
  "CHANGELOG.md",
35
35
  "LICENSE",
36
- "THIRD-PARTY-NOTICES.txt",
37
36
  "dist/types"
38
37
  ],
39
38
  "type": "module",
@@ -100,6 +99,7 @@
100
99
  "types": "./dist/types/providers/*.d.ts",
101
100
  "import": "./src/providers/*.ts"
102
101
  },
102
+ "./providers/*.js": "./src/providers/*.ts",
103
103
  "./providers/openai-codex/*": {
104
104
  "types": "./dist/types/providers/openai-codex/*.d.ts",
105
105
  "import": "./src/providers/openai-codex/*.ts"
@@ -155,16 +155,16 @@
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.13.2",
159
- "@oh-my-pi/pi-catalog": "18.13.2",
160
- "@oh-my-pi/pi-natives": "18.13.2",
161
- "@oh-my-pi/pi-utils": "18.13.2",
162
- "@oh-my-pi/pi-wire": "18.13.2"
158
+ "@oh-my-pi/omptype": "18.14.0",
159
+ "@oh-my-pi/pi-catalog": "18.14.0",
160
+ "@oh-my-pi/pi-natives": "18.14.0",
161
+ "@oh-my-pi/pi-utils": "18.14.0",
162
+ "@oh-my-pi/pi-wire": "18.14.0"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
166
166
  },
167
167
  "engines": {
168
- "bun": ">=1.3.14"
168
+ "bun": ">=1.4.4"
169
169
  }
170
170
  }
package/src/auth/abort.ts CHANGED
@@ -3,11 +3,16 @@ import * as AIError from "../error";
3
3
  /**
4
4
  * Race `promise` against `signal`, rejecting only this caller when the signal
5
5
  * fires. The underlying promise keeps running so other awaiters on the same
6
- * single-flight operation aren't punished by a peer's cancel.
6
+ * single-flight operation aren't punished by a peer's cancel. It stays
7
+ * observed either way: a caller that leaves must not turn the work's later
8
+ * rejection into an unhandled one.
7
9
  */
8
10
  export function raceSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined, message: string): Promise<T> {
9
11
  if (!signal) return promise;
10
- if (signal.aborted) return Promise.reject(new AIError.AbortError(message));
12
+ if (signal.aborted) {
13
+ void promise.catch(() => {});
14
+ return Promise.reject(new AIError.AbortError(message));
15
+ }
11
16
  const abort = Promise.withResolvers<never>();
12
17
  const onAbort = (): void => abort.reject(new AIError.AbortError(message));
13
18
  signal.addEventListener("abort", onAbort, { once: true });
@@ -369,6 +369,13 @@ export class KeyCascade implements KeysApi {
369
369
  }
370
370
  }
371
371
 
372
+ /** Warm selection metadata during session startup without choosing or pinning an account. */
373
+ async prefetchUsage(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<void> {
374
+ options?.signal?.throwIfAborted();
375
+ if (this.source(provider)?.kind !== "oauth") return;
376
+ await this.#deps.selector.prefetchUsage(provider, sessionId, options);
377
+ }
378
+
372
379
  async #resolve(
373
380
  provider: string,
374
381
  sessionId?: string,
package/src/auth/rank.ts CHANGED
@@ -111,14 +111,16 @@ function compareUsageRankedCandidatePriority(
111
111
  const leftHot = left.primaryUsed >= PRIMARY_WINDOW_HOT_FRACTION;
112
112
  const rightHot = right.primaryUsed >= PRIMARY_WINDOW_HOT_FRACTION;
113
113
  if (leftHot !== rightHot) return leftHot ? 1 : -1;
114
- // Usage-backed candidates outrank unmeasured ones: required-drain
115
- // scores are only comparable between measured windows, and the
116
- // clockless headroom fallback (0..1) must not let an account whose
114
+ // Configured priority is the user's order; a failed usage fetch says nothing
115
+ // about it, so it ranks ahead of measurement.
116
+ if (left.accountPriority !== right.accountPriority) return right.accountPriority - left.accountPriority;
117
+ // Within a priority, usage-backed candidates outrank unmeasured ones:
118
+ // required-drain scores are only comparable between measured windows, and
119
+ // the clockless headroom fallback (0..1) must not let an account whose
117
120
  // usage fetch failed shadow a measured sibling.
118
121
  const leftMeasured = left.usageMeasured;
119
122
  const rightMeasured = right.usageMeasured;
120
123
  if (leftMeasured !== rightMeasured) return leftMeasured ? -1 : 1;
121
- if (left.accountPriority !== right.accountPriority) return right.accountPriority - left.accountPriority;
122
124
  // Required drain, descending: the account whose remaining quota must
123
125
  // burn fastest to avoid expiring unused at its reset comes first, so
124
126
  // staggered resets land at ~100% utilization instead of stranding
@@ -1,3 +1,4 @@
1
+ import { scheduler } from "node:timers/promises";
1
2
  import { isSqliteBusyError, logger, postmortem } from "@oh-my-pi/pi-utils";
2
3
  import * as AIError from "../error";
3
4
  import { getOAuthProvider, normalizeOAuthCredentialExpiry, refreshOAuthToken } from "../registry/oauth";
@@ -391,7 +392,7 @@ export class OAuthRefresher {
391
392
  async #drainRefreshes(): Promise<void> {
392
393
  this.#shuttingDown = true;
393
394
  // Work tracked while draining (a lease hand-back a returning caller left behind) is waited for too.
394
- while (this.#pendingRefreshes.size > 0) await Promise.allSettled([...this.#pendingRefreshes]);
395
+ while (this.#pendingRefreshes.size > 0) await Promise.allSettled(this.#pendingRefreshes);
395
396
  }
396
397
 
397
398
  async #refreshStored<T extends OAuthCredential = OAuthCredential>(
@@ -620,18 +621,19 @@ export class OAuthRefresher {
620
621
  let keepLease = false;
621
622
  let saved = false;
622
623
  try {
623
- let stopLeaseRenewal = false;
624
+ const leaseRenewalStop = new AbortController();
624
625
  let leaseRenewalError: unknown;
625
- const leaseRenewalStopped = Promise.withResolvers<void>();
626
626
  const leaseRenewal =
627
627
  leasedCredentialId !== undefined && leaseStore
628
628
  ? (async () => {
629
629
  // Renew every interval; after a busy store, retry almost at once,
630
630
  // since another full interval would outlive the lease.
631
631
  let delayMs = OAUTH_REFRESH_LEASE_RENEW_MS;
632
- while (!stopLeaseRenewal) {
633
- await Promise.race([Bun.sleep(delayMs), leaseRenewalStopped.promise]);
634
- if (stopLeaseRenewal) return;
632
+ while (!leaseRenewalStop.signal.aborted) {
633
+ // Stopping cancels the wait, so a finished exchange leaves no renewal
634
+ // timer behind to hold the process open for up to an interval.
635
+ await scheduler.wait(delayMs, { signal: leaseRenewalStop.signal }).catch(() => undefined);
636
+ if (leaseRenewalStop.signal.aborted) return;
635
637
  let renewed: boolean;
636
638
  try {
637
639
  renewed = leaseStore.renewCredentialRefreshLease(
@@ -694,8 +696,7 @@ export class OAuthRefresher {
694
696
  until: takenOverFrom.expiresAtMs + OAUTH_REFRESH_STALLED_OWNER_MS,
695
697
  });
696
698
  // Stop renewing first: a renewal after the hand-back would take it back.
697
- stopLeaseRenewal = true;
698
- leaseRenewalStopped.resolve();
699
+ leaseRenewalStop.abort();
699
700
  await leaseRenewal;
700
701
  // Never release this lease afterwards: a hand-back that landed left
701
702
  // nothing to release, and one that did not must leave a recent owner.
@@ -754,8 +755,7 @@ export class OAuthRefresher {
754
755
  },
755
756
  };
756
757
  } finally {
757
- stopLeaseRenewal = true;
758
- leaseRenewalStopped.resolve();
758
+ leaseRenewalStop.abort();
759
759
  await leaseRenewal;
760
760
  clearTimeout(backstop);
761
761
  }
@@ -922,6 +922,9 @@ export class OAuthRefresher {
922
922
  }
923
923
  }
924
924
  if (Date.now() + OAUTH_REFRESH_SKEW_MS < credential.expires) return credential;
925
+ // A caller that already left must not own a refresh that later callers join:
926
+ // they would inherit its cancellation instead of a credential.
927
+ if (signal?.aborted) throw new AIError.AbortError("credential refresh aborted");
925
928
  if (credentialId === undefined) {
926
929
  return this.#refreshOAuthCredentialUnshared(provider, credential, undefined, signal, reason, rejectedAccess);
927
930
  }
@@ -1105,6 +1108,8 @@ export class OAuthRefresher {
1105
1108
  if (recent && recent.credential.access !== options?.rejectedAccess) {
1106
1109
  return snapshotEntry(id, recent.provider, recent.credential);
1107
1110
  }
1111
+ // A caller that already left must not own a refresh that later callers join.
1112
+ if (signal?.aborted) throw new AIError.AbortError("credential refresh aborted");
1108
1113
 
1109
1114
  const promise = (async () => {
1110
1115
  this.#deps.pool.bump("credential-refresh-start");