@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.
- package/CHANGELOG.md +41 -36
- package/dist/types/auth/abort.d.ts +3 -1
- package/dist/types/auth/cascade.d.ts +2 -0
- package/dist/types/auth/select.d.ts +2 -0
- package/dist/types/auth/store.d.ts +1 -1
- package/dist/types/auth/types.d.ts +9 -2
- package/dist/types/auth/usage.d.ts +1 -0
- package/dist/types/auth-broker/client.d.ts +3 -2
- package/dist/types/auth-broker/remote-store.d.ts +3 -3
- package/dist/types/auth-gateway/dispatch.d.ts +17 -1
- package/dist/types/error/abort.d.ts +5 -0
- package/dist/types/error/finalize.d.ts +5 -4
- package/dist/types/index.d.ts +3 -0
- package/dist/types/providers/anthropic-headers.d.ts +84 -0
- package/dist/types/providers/anthropic-http-options.d.ts +15 -0
- package/dist/types/providers/anthropic.d.ts +1 -60
- package/dist/types/providers/cursor.d.ts +2 -1
- package/dist/types/providers/openai-codex-responses.d.ts +110 -37
- package/dist/types/providers/openai-codex-state.d.ts +43 -0
- package/dist/types/providers/openai-completions.d.ts +16 -0
- package/dist/types/providers/openai-responses-state.d.ts +48 -0
- package/dist/types/providers/openai-responses.d.ts +2 -34
- package/dist/types/providers/pi-native-server.d.ts +2 -1
- package/dist/types/providers/register-builtins.d.ts +3 -3
- package/dist/types/providers/transform-messages.d.ts +9 -0
- package/dist/types/utils/abort.d.ts +4 -1
- package/package.json +8 -8
- package/src/auth/abort.ts +7 -2
- package/src/auth/cascade.ts +7 -0
- package/src/auth/rank.ts +6 -4
- package/src/auth/refresh.ts +15 -10
- package/src/auth/select.ts +98 -52
- package/src/auth/store.ts +1 -1
- package/src/auth/types.ts +9 -2
- package/src/auth/usage.ts +14 -4
- package/src/auth-broker/client.ts +7 -4
- package/src/auth-broker/remote-store.ts +47 -26
- package/src/auth-broker/server.ts +4 -1
- package/src/auth-gateway/dispatch.ts +22 -2
- package/src/auth-gateway/server.ts +18 -18
- package/src/error/abort.ts +12 -0
- package/src/error/finalize.ts +10 -5
- package/src/index.ts +3 -0
- package/src/judgment/native.ts +10 -6
- package/src/judgment/text-judge-retry.md +1 -1
- package/src/judgment/text-judge-state.md +1 -1
- package/src/judgment/text-judge.md +0 -3
- package/src/judgment/text.ts +1 -9
- package/src/provider-details.ts +1 -1
- package/src/provider-session-state.ts +1 -1
- package/src/providers/anthropic-headers.ts +412 -0
- package/src/providers/anthropic-http-options.ts +148 -0
- package/src/providers/anthropic.ts +25 -549
- package/src/providers/connection-prewarm.ts +1 -1
- package/src/providers/cowork-fetch.ts +0 -3
- package/src/providers/cursor.ts +41 -2
- package/src/providers/factory-droid/droid-identity.md +1 -1
- package/src/providers/factory-droid.ts +2 -1
- package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -1
- package/src/providers/openai-codex-responses.ts +23 -186
- package/src/providers/openai-codex-state.ts +194 -0
- package/src/providers/openai-completions.ts +40 -11
- package/src/providers/openai-responses-state.ts +100 -0
- package/src/providers/openai-responses.ts +7 -93
- package/src/providers/openai-shared.ts +6 -1
- package/src/providers/pi-native-client.ts +60 -9
- package/src/providers/pi-native-server.ts +35 -4
- package/src/providers/register-builtins.ts +68 -52
- package/src/providers/transform-messages.ts +27 -7
- package/src/registry/engine/api-key.ts +3 -1
- package/src/utils/abort.ts +11 -2
- package/src/utils/anthropic-auth.ts +5 -1
- package/THIRD-PARTY-NOTICES.txt +0 -22901
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
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.
|
|
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)
|
|
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 });
|
package/src/auth/cascade.ts
CHANGED
|
@@ -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
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
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
|
package/src/auth/refresh.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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 (!
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|