@oh-my-pi/pi-ai 15.11.3 → 15.11.4

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 (36) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/types/auth-retry.d.ts +47 -0
  3. package/dist/types/auth-storage.d.ts +17 -0
  4. package/dist/types/errors.d.ts +24 -0
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/providers/amazon-bedrock.d.ts +5 -0
  7. package/dist/types/providers/anthropic-client.d.ts +2 -2
  8. package/dist/types/providers/google-gemini-cli.d.ts +5 -0
  9. package/dist/types/providers/google-shared.d.ts +5 -0
  10. package/dist/types/providers/ollama.d.ts +5 -0
  11. package/dist/types/providers/openai-codex/request-transformer.d.ts +8 -0
  12. package/dist/types/providers/openai-codex/response-handler.d.ts +9 -0
  13. package/dist/types/providers/openai-codex-responses.d.ts +36 -1
  14. package/dist/types/providers/openai-responses-shared.d.ts +21 -3
  15. package/dist/types/providers/openai-responses.d.ts +10 -0
  16. package/dist/types/providers/pi-native-client.d.ts +9 -0
  17. package/dist/types/utils/http-inspector.d.ts +0 -1
  18. package/package.json +3 -3
  19. package/src/auth-retry.ts +97 -0
  20. package/src/auth-storage.ts +74 -14
  21. package/src/errors.ts +32 -0
  22. package/src/index.ts +1 -0
  23. package/src/providers/amazon-bedrock.ts +14 -8
  24. package/src/providers/anthropic-client.ts +4 -6
  25. package/src/providers/google-gemini-cli.ts +16 -8
  26. package/src/providers/google-shared.ts +13 -6
  27. package/src/providers/ollama.ts +10 -7
  28. package/src/providers/openai-codex/request-transformer.ts +45 -0
  29. package/src/providers/openai-codex/response-handler.ts +21 -0
  30. package/src/providers/openai-codex-responses.ts +267 -83
  31. package/src/providers/openai-completions.ts +10 -10
  32. package/src/providers/openai-responses-shared.ts +52 -6
  33. package/src/providers/openai-responses.ts +232 -49
  34. package/src/providers/pi-native-client.ts +21 -8
  35. package/src/stream.ts +4 -4
  36. package/src/utils/http-inspector.ts +0 -10
package/CHANGELOG.md CHANGED
@@ -2,7 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.4] - 2026-06-12
6
+
7
+ ### Added
8
+
9
+ - Codex/Responses providers now map `end_turn: false` on the terminal stream event (Codex backend signal for "response ended, turn didn't" — commentary-only progress updates) to `stopDetails: { type: "pause_turn" }` with stopReason `"stop"`, so the agent loop can re-sample instead of ending the turn. Wired in `openai-codex-responses` and `processResponsesStream` (`openai-responses`/`azure-openai-responses`); inert for backends that never send the field.
10
+ - Added Codex upstream protocol features to `openai-codex-responses` (tracking codex-rs as of June 2026): `onModerationMetadata` callback surfacing `response.metadata` → `openai_chatgpt_moderation_metadata` on both transports; `reasoningContext` option emitting `reasoning.context` (`auto`/`current_turn`/`all_turns`); `clientMetadata` option emitting `client_metadata` in the request body (canonical `x-codex-turn-metadata` envelope) without breaking the websocket append fast-path; and an opt-in `responsesLite` mode mirroring codex-rs — lite header on HTTP requests and the websocket upgrade, `ws_request_header_*` marker in `response.create` client metadata, lite-keyed socket pooling, image-detail stripping, forced serial tool calls, and `reasoning.context: all_turns` default. Dormant until OpenAI flips `use_responses_lite` in the model catalog.
11
+ - Added `withOAuthAccess` — the `withAuth` counterpart for OAuth-access consumers: runs an operation through the central a/b/c auth-retry policy (resolve → force-refresh same account → rotate to a sibling) while handing the attempt the full `OAuthAccess` (bearer plus `accountId`/`projectId`/`enterpriseUrl` identity metadata). Use it instead of hand-rolled `getOAuthAccess` + fetch flows so 401s and usage-limits rotate credentials instead of failing the call.
12
+ - Added `ProviderHttpError` — a typed HTTP error carrying `status`, `headers`, and `code` — replacing the ad-hoc `as Error & { status?... }` / `Object.assign` hacks at provider throw sites, with per-provider subclasses `CodexApiError`, `AuthGatewayError`, `GoogleApiError`, `GeminiCliApiError`, `OllamaApiError`, and `BedrockApiError`; `AnthropicApiError` now extends it. Google, Gemini CLI, Ollama, and Bedrock HTTP errors now also carry response headers, so server-suggested `retry-after` delays are visible to retry classification on those paths. The internal `withHttpStatus` helper was removed.
13
+ - Added stateful SSE turn chaining for OpenAI Codex (on by default; disable with `PI_CODEX_STATEFUL=0` or `statefulResponses: false`): SSE requests now reuse `previous_response_id` with delta-only input instead of replaying the full transcript, mirroring the websocket fast-path via a shared transport-aware builder. Any history mutation or option change falls back to a full replay; a server-side `previous_response_not_found` (HTTP or in-stream) resets the chain and retries the turn with full context, and three consecutive stale failures disable chaining for the session.
14
+ - Added stateful `previous_response_id` chaining to the platform OpenAI Responses provider (`openai-responses`): on by default against the official api.openai.com endpoint (forces `store: true`, which chaining requires), off for other Responses endpoints; override with `statefulResponses` or `PI_OPENAI_STATEFUL`. Chain detection compares the wire form of the conversation arguments alone — per-turn trailing scaffolding such as the GPT-5 "Juice: 0" developer item is excluded from the append-baseline prefix check and re-appended to the delta — and a rejected/stale previous response falls back to a one-shot full replay with the same circuit breaker.
15
+ - Added `AuthStorage.getOAuthAccountIdentity()` and the `OAuthAccountIdentity` type — a read-only lookup returning the `accountId`/`email`/`projectId` of the OAuth credential a session is currently routed to, for display and metadata paths.
16
+
17
+ ### Changed
18
+
19
+ - The GPT-5 "Juice: 0" no-reasoning developer item in `applyResponsesReasoningParams` is now gated on the resolved `compat.requiresJuiceZeroHack` flag (auto-detected from GPT-5-family model names by `@oh-my-pi/pi-catalog`, overridable per model) instead of an inline model-name check.
20
+
21
+ ### Fixed
22
+
23
+ - Fixed websocket append fast-path to remain usable when only `client_metadata` changes between turns
24
+ - Fixed `onModerationMetadata` handling so exceptions thrown by callback observers no longer terminate the response stream
25
+ - Fixed local SQLite OAuth credential caches returning a stale Anthropic access token after another `omp` process refreshed and persisted the same row. `AuthStorage` now syncs the selected row from storage before returning or force-refreshing OAuth credentials, so concurrent sessions pick up peer-rotated tokens instead of surfacing a one-turn `401 Invalid authentication credentials`.
26
+ - Fixed forced OAuth preflight refresh failures being swallowed silently in credential selection; they now emit a debug log (`OAuth preflight refresh failed`) so stale-refresh-token replays from concurrent sessions are diagnosable.
27
+
5
28
  ## [15.11.3] - 2026-06-11
29
+
6
30
  ### Fixed
7
31
 
8
32
  - Fixed GitHub Copilot long-context model requests to use the upstream `requestModelId` when calling Anthropic, OpenAI Responses, and OpenAI Completions APIs
@@ -3274,4 +3298,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
3274
3298
 
3275
3299
  ## [0.9.4] - 2025-11-26
3276
3300
 
3277
- Initial release with multi-provider LLM support.
3301
+ Initial release with multi-provider LLM support.
@@ -1,3 +1,4 @@
1
+ import type { OAuthAccess } from "./auth-storage";
1
2
  /**
2
3
  * Context passed to an {@link ApiKeyResolver} on each resolution attempt.
3
4
  *
@@ -70,3 +71,49 @@ export declare function withAuth<T>(key: ApiKey | undefined, attempt: (key: stri
70
71
  signal?: AbortSignal;
71
72
  missingKeyMessage?: string;
72
73
  }): Promise<T>;
74
+ /**
75
+ * Minimal structural slice of `AuthStorage` consumed by {@link withOAuthAccess}.
76
+ * Typed structurally (and importing only the `OAuthAccess` type) so this module
77
+ * never takes a runtime dependency on `./auth-storage`.
78
+ */
79
+ export interface OAuthAccessSource {
80
+ getOAuthAccess(provider: string, sessionId?: string, options?: {
81
+ forceRefresh?: boolean;
82
+ signal?: AbortSignal;
83
+ }): Promise<OAuthAccess | undefined>;
84
+ rotateSessionCredential(provider: string, sessionId: string | undefined, options?: {
85
+ error?: unknown;
86
+ signal?: AbortSignal;
87
+ }): Promise<boolean>;
88
+ }
89
+ export interface WithOAuthAccessOptions {
90
+ /** Session id for credential stickiness, threaded into every resolve. */
91
+ sessionId?: string;
92
+ signal?: AbortSignal;
93
+ /** Override the retryable-error classifier (default {@link isAuthRetryableError}). */
94
+ isAuthError?: (error: unknown) => boolean;
95
+ /**
96
+ * Pre-resolved access used for the initial attempt. Callers that already
97
+ * resolved access for an availability gate pass it here so the helper
98
+ * doesn't double-resolve (mirrors the gateway resolver's `initialKey`).
99
+ */
100
+ seed?: OAuthAccess;
101
+ missingAccessMessage?: string;
102
+ }
103
+ /**
104
+ * {@link withAuth} for OAuth-access consumers: runs an auth-protected
105
+ * operation through the central a/b/c retry policy, handing the attempt the
106
+ * full {@link OAuthAccess} (bearer + identity metadata: `accountId`,
107
+ * `projectId`, `enterpriseUrl`) instead of bare API-key bytes.
108
+ *
109
+ * - initial → `getOAuthAccess` (or `opts.seed`).
110
+ * - step (b) → `getOAuthAccess` with `forceRefresh: true` (re-mint the SAME
111
+ * account; picks up peer/broker rotations).
112
+ * - step (c) → `rotateSessionCredential` then re-resolve (switch to a sibling).
113
+ *
114
+ * A step is skipped when it yields no access or the same `accessToken` that
115
+ * just failed; non-auth errors propagate immediately. Use this instead of
116
+ * hand-rolled `getOAuthAccess` + fetch flows so 401s and usage-limits rotate
117
+ * credentials instead of failing the call.
118
+ */
119
+ export declare function withOAuthAccess<T>(storage: OAuthAccessSource, provider: string, attempt: (access: OAuthAccess) => Promise<T>, opts?: WithOAuthAccessOptions): Promise<T>;
@@ -430,6 +430,17 @@ export interface OAuthAccessFailure {
430
430
  enterpriseUrl?: string;
431
431
  error: string;
432
432
  }
433
+ /**
434
+ * Identity of the OAuth credential a session is currently routed to. Read-only
435
+ * display/metadata shape: `accountId` is the provider's account UUID, `email`
436
+ * the user-facing login, `projectId` the GCP-style project for providers that
437
+ * key usage on it (Gemini CLI / Antigravity).
438
+ */
439
+ export interface OAuthAccountIdentity {
440
+ accountId?: string;
441
+ email?: string;
442
+ projectId?: string;
443
+ }
433
444
  export type OAuthAccessResolution = ({
434
445
  ok: true;
435
446
  } & OAuthAccess) | ({
@@ -581,6 +592,12 @@ export declare class AuthStorage {
581
592
  * Returns `undefined` when no OAuth credential carries an `accountId`.
582
593
  */
583
594
  getOAuthAccountId(provider: string, sessionId?: string): string | undefined;
595
+ /**
596
+ * Get the OAuth account identity for a provider, preferring the credential that
597
+ * is session-sticky for `sessionId`. This is a read-only lookup for display and
598
+ * metadata paths; it does not refresh tokens, rank usage, or advance selection.
599
+ */
600
+ getOAuthAccountIdentity(provider: string, sessionId?: string): OAuthAccountIdentity | undefined;
584
601
  /**
585
602
  * Get all credentials.
586
603
  */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Structured HTTP errors thrown by provider clients.
3
+ *
4
+ * Downstream classification reads these fields structurally rather than via
5
+ * `instanceof`: `extractHttpStatusFromError` (pi-utils) reads `status`,
6
+ * `getHeadersFromError` (retry-after extraction) reads `headers`, and retry
7
+ * policies such as `isCopilotTransientModelError` read `code`. Per-provider
8
+ * subclasses exist so call sites can narrow with `instanceof` and logs carry
9
+ * a meaningful `error.name`.
10
+ */
11
+ export interface ProviderHttpErrorOptions {
12
+ /** Response headers; enables `retry-after`/rate-limit extraction downstream. */
13
+ headers?: Headers;
14
+ /** Machine-readable error code from the response body (`error.code` / `error.type`). */
15
+ code?: string;
16
+ cause?: unknown;
17
+ }
18
+ /** Non-2xx HTTP response from a provider endpoint. */
19
+ export declare class ProviderHttpError extends Error {
20
+ readonly status: number;
21
+ readonly headers: Headers | undefined;
22
+ readonly code: string | undefined;
23
+ constructor(message: string, status: number, options?: ProviderHttpErrorOptions);
24
+ }
@@ -5,6 +5,7 @@ export { type AuthGatewayBootOptions, type ModelResolver, startAuthGateway } fro
5
5
  export * from "./auth-gateway/types";
6
6
  export * from "./auth-retry";
7
7
  export * from "./auth-storage";
8
+ export * from "./errors";
8
9
  export * from "./provider-details";
9
10
  export * from "./providers/anthropic";
10
11
  export * from "./providers/anthropic-client";
@@ -7,7 +7,12 @@
7
7
  * Bun's native `HTTPS_PROXY` support.
8
8
  */
9
9
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
10
+ import { ProviderHttpError } from "../errors";
10
11
  import type { StreamFunction, StreamOptions, ThinkingBudgets } from "../types";
12
+ /** Non-2xx response (or in-stream exception event) from the Bedrock runtime API. */
13
+ export declare class BedrockApiError extends ProviderHttpError {
14
+ readonly name = "BedrockApiError";
15
+ }
11
16
  export type BedrockThinkingDisplay = "summarized" | "omitted";
12
17
  export interface BedrockOptions extends StreamOptions {
13
18
  region?: string;
@@ -1,3 +1,4 @@
1
+ import { ProviderHttpError } from "../errors";
1
2
  import type { FetchImpl } from "../types";
2
3
  import type { MessageCreateParamsStreaming } from "./anthropic-wire";
3
4
  /** Per-request options accepted by {@link AnthropicMessages.create}. */
@@ -39,8 +40,7 @@ export interface AnthropicClientOptions {
39
40
  fetchOptions?: AnthropicFetchOptions;
40
41
  }
41
42
  /** Non-2xx response from the Anthropic API. */
42
- export declare class AnthropicApiError extends Error {
43
- readonly status: number;
43
+ export declare class AnthropicApiError extends ProviderHttpError {
44
44
  readonly headers: Headers;
45
45
  readonly requestId: string | null;
46
46
  constructor(status: number, message: string, headers: Headers);
@@ -1,3 +1,4 @@
1
+ import { ProviderHttpError } from "../errors";
1
2
  import type { Context, Model, StreamFunction, StreamOptions } from "../types";
2
3
  import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "./google-shared";
3
4
  import { type GoogleThinkingLevel } from "./google-shared";
@@ -6,6 +7,10 @@ import { type GoogleThinkingLevel } from "./google-shared";
6
7
  * `import { GoogleThinkingLevel } from "./google-gemini-cli"` callers keep working.
7
8
  */
8
9
  export type { GoogleThinkingLevel };
10
+ /** Non-2xx response (or in-stream error chunk) from the Cloud Code Assist API. */
11
+ export declare class GeminiCliApiError extends ProviderHttpError {
12
+ readonly name = "GeminiCliApiError";
13
+ }
9
14
  export interface GoogleGeminiCliOptions extends StreamOptions {
10
15
  /**
11
16
  * Tool selection mode. String forms map directly to Gemini
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
3
3
  */
4
+ import { ProviderHttpError } from "../errors";
4
5
  import type { AssistantMessage, Context, FetchImpl, Model, StopReason, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall } from "../types";
5
6
  import { AssistantMessageEventStream } from "../utils/event-stream";
6
7
  import { normalizeSchemaForGoogle } from "../utils/schema";
7
8
  import type { Content, FinishReason, FunctionCallingConfigMode, GenerateContentParameters, GenerateContentResponse, Part } from "./google-types";
8
9
  export type { Content, FunctionCallingConfigMode, GenerateContentParameters, GenerateContentResponse, ThinkingConfig, } from "./google-types";
9
10
  export { normalizeSchemaForGoogle };
11
+ /** Non-2xx response (or in-stream error chunk) from the Google Generative Language / Vertex API. */
12
+ export declare class GoogleApiError extends ProviderHttpError {
13
+ readonly name = "GoogleApiError";
14
+ }
10
15
  type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
11
16
  /**
12
17
  * Thinking level for Gemini 3 models. Mirrors Google's `ThinkingLevel` enum values.
@@ -1,4 +1,9 @@
1
+ import { ProviderHttpError } from "../errors";
1
2
  import type { StreamFunction, StreamOptions, ToolChoice } from "../types";
3
+ /** Non-2xx response from the Ollama `/api/chat` endpoint. */
4
+ export declare class OllamaApiError extends ProviderHttpError {
5
+ readonly name = "OllamaApiError";
6
+ }
2
7
  export interface OllamaChatOptions extends StreamOptions {
3
8
  reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
4
9
  disableReasoning?: boolean;
@@ -1,13 +1,20 @@
1
1
  import type { Api, Model } from "../../types";
2
+ /** Reasoning replay scope for the Codex Responses API (`reasoning.context`). */
3
+ export type CodexReasoningContext = "auto" | "current_turn" | "all_turns";
2
4
  export interface ReasoningConfig {
3
5
  effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
4
6
  summary?: "auto" | "concise" | "detailed";
7
+ context?: CodexReasoningContext;
5
8
  }
6
9
  export interface CodexRequestOptions {
7
10
  reasoningEffort?: ReasoningConfig["effort"];
8
11
  reasoningSummary?: ReasoningConfig["summary"] | null;
12
+ /** Explicit `reasoning.context` override. Defaults to `all_turns` under {@link CodexRequestOptions.responsesLite}, otherwise omitted (server default is `current_turn`). */
13
+ reasoningContext?: CodexReasoningContext;
9
14
  textVerbosity?: "low" | "medium" | "high";
10
15
  include?: string[];
16
+ /** Responses Lite transport contract: strips image detail and defaults `reasoning.context` to `all_turns`, mirroring codex-rs. */
17
+ responsesLite?: boolean;
11
18
  }
12
19
  export interface InputItem {
13
20
  id?: string | null;
@@ -40,6 +47,7 @@ export interface RequestBody {
40
47
  include?: string[];
41
48
  prompt_cache_key?: string;
42
49
  prompt_cache_retention?: "in_memory" | "24h";
50
+ client_metadata?: Record<string, string>;
43
51
  max_output_tokens?: number;
44
52
  max_completion_tokens?: number;
45
53
  [key: string]: unknown;
@@ -1,3 +1,4 @@
1
+ import { ProviderHttpError } from "../../errors";
1
2
  export type CodexRateLimit = {
2
3
  used_percent?: number;
3
4
  window_minutes?: number;
@@ -10,8 +11,16 @@ export type CodexRateLimits = {
10
11
  export type CodexErrorInfo = {
11
12
  message: string;
12
13
  status: number;
14
+ /** Machine-readable error code (`error.code` or `error.type` from the response body), when present. */
15
+ code?: string;
13
16
  friendlyMessage?: string;
14
17
  rateLimits?: CodexRateLimits;
15
18
  raw?: string;
16
19
  };
20
+ /** Non-2xx response from the Codex backend, with the parsed body retained. */
21
+ export declare class CodexApiError extends ProviderHttpError {
22
+ readonly info: CodexErrorInfo;
23
+ constructor(info: CodexErrorInfo, headers?: Headers);
24
+ static fromResponse(response: Response): Promise<CodexApiError>;
25
+ }
17
26
  export declare function parseCodexError(response: Response): Promise<CodexErrorInfo>;
@@ -1,16 +1,51 @@
1
1
  import type { ResponseInput } from "openai/resources/responses/responses";
2
2
  import { type Context, type Model, type ProviderSessionState, type ServiceTier, type StreamFunction, type StreamOptions, type Tool, type ToolChoice } from "../types";
3
+ import { type CodexReasoningContext } from "./openai-codex/request-transformer";
3
4
  export interface OpenAICodexResponsesOptions extends StreamOptions {
4
5
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
5
6
  reasoningSummary?: "auto" | "concise" | "detailed" | null;
7
+ /** `reasoning.context` replay scope. Defaults to `all_turns` under {@link OpenAICodexResponsesOptions.responsesLite}, otherwise omitted (server default is `current_turn`). */
8
+ reasoningContext?: CodexReasoningContext;
6
9
  textVerbosity?: "low" | "medium" | "high";
7
10
  include?: string[];
8
11
  codexMode?: boolean;
9
12
  toolChoice?: ToolChoice;
10
13
  preferWebsockets?: boolean;
14
+ /**
15
+ * Enable stateful SSE turns: chain via `previous_response_id` + delta input
16
+ * instead of replaying the full transcript. Requires `sessionId` +
17
+ * `providerSessionState`. `false` vetoes the `PI_CODEX_STATEFUL` env flag.
18
+ */
19
+ statefulResponses?: boolean;
11
20
  serviceTier?: ServiceTier;
21
+ /**
22
+ * Opt into the Responses Lite transport contract. Sends
23
+ * `x-openai-internal-codex-responses-lite: true` on HTTP requests and on the
24
+ * WebSocket upgrade (the marker is connection-scoped there, so lite and
25
+ * non-lite turns never share a pooled socket), strips image detail from
26
+ * input, and defaults `reasoning.context` to `all_turns` — mirroring codex-rs.
27
+ */
28
+ responsesLite?: boolean;
29
+ /**
30
+ * Extra `client_metadata` to include in the request body on both transports.
31
+ * The canonical Codex envelope is `client_metadata["x-codex-turn-metadata"]`
32
+ * (JSON string of thread/turn identifiers); flat keys are also accepted.
33
+ */
34
+ clientMetadata?: Record<string, string>;
35
+ /**
36
+ * Invoked when the server streams a `response.metadata` event carrying
37
+ * ChatGPT moderation metadata (`metadata.openai_chatgpt_moderation_metadata`)
38
+ * for first-party presentation parity. Diagnostic observer: failures are
39
+ * swallowed and must not alter the stream.
40
+ */
41
+ onModerationMetadata?: (metadata: unknown) => void;
12
42
  }
13
43
  type CodexTransport = "sse" | "websocket";
44
+ /**
45
+ * Per-session request-shape counters. Despite the name, these cover both
46
+ * transports: once stateful SSE chaining is enabled, SSE requests are counted
47
+ * too (the shared chained-request builder records every request it shapes).
48
+ */
14
49
  export interface OpenAICodexWebSocketDebugStats {
15
50
  fullContextRequests: number;
16
51
  deltaRequests: number;
@@ -21,7 +56,7 @@ export interface OpenAICodexWebSocketDebugStats {
21
56
  /** @internal Exported for tests. */
22
57
  export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined, tools?: Tool[], model?: Model<"openai-codex-responses">): string | Record<string, unknown> | undefined;
23
58
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
24
- export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
59
+ export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite">): Promise<void>;
25
60
  export interface OpenAICodexTransportDetails {
26
61
  websocketPreferred: boolean;
27
62
  lastTransport?: CodexTransport;
@@ -100,8 +100,12 @@ type ReasoningOptions = {
100
100
  };
101
101
  /**
102
102
  * Apply reasoning-related Responses parameters: enable encrypted reasoning content for replay,
103
- * set effort/summary when requested, and otherwise inject the GPT-5 "Juice: 0" no-reasoning hack.
104
- * Mutates `params` and may push a developer message into `messages`.
103
+ * set effort/summary when requested, and otherwise inject the "Juice: 0" no-reasoning hack
104
+ * when `model.compat.requiresJuiceZeroHack` is set (GPT-5 family by default).
105
+ * Mutates `params` and may push a developer message into `messages`. Returns
106
+ * the number of per-turn trailing scaffolding items appended to `messages`
107
+ * (the "Juice: 0" developer item), so callers doing stateful
108
+ * `previous_response_id` chaining can exclude them from append-baseline math.
105
109
  *
106
110
  * @param omitReasoningEffort - When `true`, suppresses `params.reasoning.effort` from the wire
107
111
  * body. Set by `xai-responses.ts` via {@link OpenAIResponsesOptions.omitReasoningEffort} for
@@ -112,7 +116,7 @@ type ReasoningOptions = {
112
116
  * without needing explicit activation. Callers that pass `options.reasoning` for such models
113
117
  * should expect this documented downgrade: the model will reason, but at its default effort.
114
118
  */
115
- export declare function applyResponsesReasoningParams<P extends OpenAI.Responses.ResponseCreateParamsStreaming>(params: P, model: Model<Api>, options: ReasoningOptions | undefined, messages: ResponseInput, mapEffort?: (effort: string) => string, includeEncryptedReasoning?: boolean, omitReasoningEffort?: boolean): void;
119
+ export declare function applyResponsesReasoningParams<P extends OpenAI.Responses.ResponseCreateParamsStreaming>(params: P, model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">, options: ReasoningOptions | undefined, messages: ResponseInput, mapEffort?: (effort: string) => string, includeEncryptedReasoning?: boolean, omitReasoningEffort?: boolean): number;
116
120
  /** Populate `output.usage` from a Responses-API `response.usage` payload. Does not invoke `calculateCost`. */
117
121
  export declare function populateResponsesUsageFromResponse(output: AssistantMessage, usage: {
118
122
  input_tokens?: number | null;
@@ -125,4 +129,18 @@ export declare function populateResponsesUsageFromResponse(output: AssistantMess
125
129
  reasoning_tokens?: number | null;
126
130
  } | null;
127
131
  } | null | undefined): void;
132
+ /**
133
+ * Strict-prefix delta for stateful `previous_response_id` chaining (used by the
134
+ * platform Responses provider and the Codex provider on both transports):
135
+ * returns the input items the current request appends beyond the previous
136
+ * request's input plus the previous response's output items, or null when the
137
+ * request options differ or history mutated (the chain must break). Per-turn
138
+ * `client_metadata` (e.g. rotating turn ids) is excluded from the option
139
+ * comparison; codex-rs excludes it from the same check.
140
+ */
141
+ export declare function buildResponsesDeltaInput<TItem>(previous: {
142
+ input?: unknown;
143
+ } | undefined, previousResponseItems: readonly TItem[] | undefined, current: {
144
+ input?: unknown;
145
+ }): TItem[] | null;
128
146
  export {};
@@ -7,6 +7,16 @@ export interface OpenAIResponsesOptions extends StreamOptions {
7
7
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
8
8
  serviceTier?: ServiceTier;
9
9
  toolChoice?: ToolChoice;
10
+ /**
11
+ * Stateful turns: chain via `previous_response_id` + delta input instead of
12
+ * replaying the full transcript. Forces `store: true` (the platform only
13
+ * resolves stored responses). Defaults ON against the official OpenAI API
14
+ * and OFF for other Responses endpoints; `PI_OPENAI_STATEFUL` overrides the
15
+ * default, and `false` here vetoes everything. Requires `sessionId` +
16
+ * `providerSessionState`. Falls back to a full replay whenever history
17
+ * mutates or the server reports a stale id.
18
+ */
19
+ statefulResponses?: boolean;
10
20
  /**
11
21
  * Enforce strict tool call/result pairing when building Responses API inputs.
12
22
  * Azure OpenAI and GitHub Copilot Responses paths require tool results to match prior tool calls.
@@ -1,4 +1,13 @@
1
+ import { ProviderHttpError } from "../errors";
1
2
  import type { Api, AssistantMessageEventStream as AssistantMessageEventStreamType, Context, Model, SimpleStreamOptions } from "../types";
3
+ /**
4
+ * Non-2xx response from the auth-gateway `/v1/pi/stream` endpoint. `code`
5
+ * carries the gateway's error-type token (`authentication_error`,
6
+ * `rate_limit_error`, `upstream_error`, ...).
7
+ */
8
+ export declare class AuthGatewayError extends ProviderHttpError {
9
+ constructor(message: string, status: number, headers?: Headers, code?: string);
10
+ }
2
11
  /**
3
12
  * Stream a turn through an `omp auth-gateway` over the pi-native protocol.
4
13
  *
@@ -15,7 +15,6 @@ export type CapturedHttpErrorResponse = {
15
15
  };
16
16
  export declare function appendRawHttpRequestDumpFor400(message: string, error: unknown, dump: RawHttpRequestDump | undefined): Promise<string>;
17
17
  export declare function finalizeErrorMessage(error: unknown, rawRequestDump: RawHttpRequestDump | undefined, capturedErrorResponse?: CapturedHttpErrorResponse): Promise<string>;
18
- export declare function withHttpStatus(error: unknown, status: number): Error;
19
18
  /**
20
19
  * Rewrite error message for GitHub Copilot request failures.
21
20
  * Must run AFTER finalizeErrorMessage since it replaces the message entirely.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.11.3",
4
+ "version": "15.11.4",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "15.11.3",
42
- "@oh-my-pi/pi-utils": "15.11.3",
41
+ "@oh-my-pi/pi-catalog": "15.11.4",
42
+ "@oh-my-pi/pi-utils": "15.11.4",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
package/src/auth-retry.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
2
+ import type { OAuthAccess } from "./auth-storage";
2
3
  import { isUsageLimitError } from "./rate-limit-utils";
3
4
 
4
5
  /**
@@ -139,3 +140,99 @@ export async function withAuth<T>(
139
140
 
140
141
  throw lastError;
141
142
  }
143
+
144
+ /**
145
+ * Minimal structural slice of `AuthStorage` consumed by {@link withOAuthAccess}.
146
+ * Typed structurally (and importing only the `OAuthAccess` type) so this module
147
+ * never takes a runtime dependency on `./auth-storage`.
148
+ */
149
+ export interface OAuthAccessSource {
150
+ getOAuthAccess(
151
+ provider: string,
152
+ sessionId?: string,
153
+ options?: { forceRefresh?: boolean; signal?: AbortSignal },
154
+ ): Promise<OAuthAccess | undefined>;
155
+ rotateSessionCredential(
156
+ provider: string,
157
+ sessionId: string | undefined,
158
+ options?: { error?: unknown; signal?: AbortSignal },
159
+ ): Promise<boolean>;
160
+ }
161
+
162
+ export interface WithOAuthAccessOptions {
163
+ /** Session id for credential stickiness, threaded into every resolve. */
164
+ sessionId?: string;
165
+ signal?: AbortSignal;
166
+ /** Override the retryable-error classifier (default {@link isAuthRetryableError}). */
167
+ isAuthError?: (error: unknown) => boolean;
168
+ /**
169
+ * Pre-resolved access used for the initial attempt. Callers that already
170
+ * resolved access for an availability gate pass it here so the helper
171
+ * doesn't double-resolve (mirrors the gateway resolver's `initialKey`).
172
+ */
173
+ seed?: OAuthAccess;
174
+ missingAccessMessage?: string;
175
+ }
176
+
177
+ /**
178
+ * {@link withAuth} for OAuth-access consumers: runs an auth-protected
179
+ * operation through the central a/b/c retry policy, handing the attempt the
180
+ * full {@link OAuthAccess} (bearer + identity metadata: `accountId`,
181
+ * `projectId`, `enterpriseUrl`) instead of bare API-key bytes.
182
+ *
183
+ * - initial → `getOAuthAccess` (or `opts.seed`).
184
+ * - step (b) → `getOAuthAccess` with `forceRefresh: true` (re-mint the SAME
185
+ * account; picks up peer/broker rotations).
186
+ * - step (c) → `rotateSessionCredential` then re-resolve (switch to a sibling).
187
+ *
188
+ * A step is skipped when it yields no access or the same `accessToken` that
189
+ * just failed; non-auth errors propagate immediately. Use this instead of
190
+ * hand-rolled `getOAuthAccess` + fetch flows so 401s and usage-limits rotate
191
+ * credentials instead of failing the call.
192
+ */
193
+ export async function withOAuthAccess<T>(
194
+ storage: OAuthAccessSource,
195
+ provider: string,
196
+ attempt: (access: OAuthAccess) => Promise<T>,
197
+ opts?: WithOAuthAccessOptions,
198
+ ): Promise<T> {
199
+ const isAuthError = opts?.isAuthError ?? isAuthRetryableError;
200
+ const { sessionId, signal } = opts ?? {};
201
+
202
+ let lastAccess = opts?.seed ?? (await storage.getOAuthAccess(provider, sessionId, { signal }));
203
+ if (!lastAccess) {
204
+ throw new Error(opts?.missingAccessMessage ?? `No OAuth credential available for provider: ${provider}`);
205
+ }
206
+
207
+ const resolveStep = async (lastChance: boolean, error: unknown): Promise<OAuthAccess | undefined> => {
208
+ try {
209
+ if (!lastChance) return await storage.getOAuthAccess(provider, sessionId, { forceRefresh: true, signal });
210
+ await storage.rotateSessionCredential(provider, sessionId, { error, signal });
211
+ return await storage.getOAuthAccess(provider, sessionId, { signal });
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ };
216
+
217
+ let lastError: unknown;
218
+ try {
219
+ return await attempt(lastAccess);
220
+ } catch (error) {
221
+ if (!isAuthError(error)) throw error;
222
+ lastError = error;
223
+ }
224
+
225
+ for (const lastChance of AUTH_RETRY_STEPS) {
226
+ const next = await resolveStep(lastChance, lastError);
227
+ if (!next || next.accessToken === lastAccess.accessToken) continue;
228
+ lastAccess = next;
229
+ try {
230
+ return await attempt(next);
231
+ } catch (error) {
232
+ if (!isAuthError(error)) throw error;
233
+ lastError = error;
234
+ }
235
+ }
236
+
237
+ throw lastError;
238
+ }