@oh-my-pi/pi-ai 15.9.67 → 15.10.1

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 (44) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/types/auth-gateway/types.d.ts +1 -1
  3. package/dist/types/auth-retry.d.ts +72 -0
  4. package/dist/types/auth-storage.d.ts +43 -0
  5. package/dist/types/effort.d.ts +9 -0
  6. package/dist/types/index.d.ts +2 -0
  7. package/dist/types/model-thinking.d.ts +1 -16
  8. package/dist/types/provider-models/openai-compat.d.ts +5 -0
  9. package/dist/types/providers/amazon-bedrock.d.ts +1 -1
  10. package/dist/types/providers/openai-responses-shared.d.ts +15 -0
  11. package/dist/types/stream.d.ts +1 -1
  12. package/dist/types/types.d.ts +11 -8
  13. package/dist/types/utils/schema/fields.d.ts +5 -0
  14. package/dist/types/utils/schema/json-schema-validator.d.ts +8 -0
  15. package/package.json +2 -2
  16. package/src/auth-gateway/server.ts +74 -26
  17. package/src/auth-gateway/types.ts +1 -1
  18. package/src/auth-retry.ts +141 -0
  19. package/src/auth-storage.ts +152 -9
  20. package/src/effort.ts +16 -0
  21. package/src/index.ts +2 -0
  22. package/src/model-thinking.ts +1 -29
  23. package/src/models.json +402 -30
  24. package/src/provider-models/ollama.ts +1 -1
  25. package/src/provider-models/openai-compat.ts +28 -2
  26. package/src/providers/amazon-bedrock.ts +1 -1
  27. package/src/providers/anthropic.ts +22 -29
  28. package/src/providers/azure-openai-responses.ts +2 -1
  29. package/src/providers/gitlab-duo.ts +3 -2
  30. package/src/providers/openai-anthropic-shim.ts +5 -2
  31. package/src/providers/openai-codex/request-transformer.ts +79 -33
  32. package/src/providers/openai-completions.ts +7 -1
  33. package/src/providers/openai-responses-shared.ts +72 -5
  34. package/src/providers/openai-responses.ts +2 -1
  35. package/src/providers/pi-native-client.ts +4 -1
  36. package/src/stream.ts +37 -17
  37. package/src/types.ts +11 -8
  38. package/src/usage/google-antigravity.ts +77 -24
  39. package/src/utils/http-inspector.ts +4 -2
  40. package/src/utils/request-debug.ts +12 -7
  41. package/src/utils/schema/fields.ts +16 -0
  42. package/src/utils/schema/json-schema-validator.ts +19 -1
  43. package/src/utils/schema/normalize.ts +23 -2
  44. package/src/utils/validation.ts +26 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.1] - 2026-06-07
6
+
7
+ ### Breaking Changes
8
+
9
+ - Removed the `onAuthError` option from stream request options and shifted auth retry handling to resolver-based `apiKey` behavior, requiring callers using custom auth-retry hooks to migrate
10
+
11
+ ### Added
12
+
13
+ - Added `ApiKeyResolver` and `ApiKey` auth helpers, including `isApiKeyResolver`, `isAuthRetryableError`, `resolveApiKeyOnce`, and `withAuth`, and exported them from the package root
14
+ - Added support for a function-valued `apiKey` in `SimpleStreamOptions` so a single stream request can refresh or rotate credentials during retry
15
+ - Added `forceRefresh` credential option to `AuthStorage.getApiKey` and `rotateSessionCredential` support for session-level credential rotation after auth failures
16
+ - Added `AuthStorage.resolver(provider, options)` method that builds an `ApiKeyResolver` implementing the a/b/c auth-retry policy directly on the storage instance
17
+
18
+ ### Changed
19
+
20
+ - Changed gateway and stream auth flows to share the a/b/c retry policy, refreshing the same session credential first and then switching to a sibling credential on repeated auth failures
21
+
22
+ ### Fixed
23
+
24
+ - Fixed streaming auth retries to handle `401` and usage-limit errors before replay-unsafe content is emitted, including failures surfaced only via `errorStatus`
25
+ - Fixed tool argument validation to coerce singleton non-string values into arrays when the schema expects an array, preventing Anthropic-compatible models that emit `todo.ops` as an object from getting stuck in repeated validation-error loops. ([#2026](https://github.com/can1357/oh-my-pi/issues/2026))
26
+ - Fixed streaming retries to buffer and suppress partial `start` events from failed auth attempts so only clean retried events are delivered
27
+ - Fixed the HTTP 400 raw-request dumper (`appendRawHttpRequestDumpFor400`) littering the real `~/.omp/logs/http-400-requests` directory during tests. Provider suites exercise the 400 error path with mocked `fetch` responses, which the dumper could not distinguish from genuine failures; it now skips persistence under the Bun test runner (`isBunTestRuntime()`).
28
+ - Fixed Anthropic Opus requests unnecessarily forcing `tool_choice.disable_parallel_tool_use`, allowing Claude Opus to use the provider's default parallel tool-calling behavior again.
29
+ - Fixed parallel `function_call` items losing arguments against llama.cpp's OpenAI Responses endpoint (`/v1/responses`), where every call but the last finalized with `{}` and the agent rejected them with `path: Invalid input: expected string, received undefined`. llama.cpp's `to_json_oaicompat_resp` emits `output_item.added` with only `item.call_id` (no `item.id`, no `output_index`) while the matching `function_call_arguments.delta` carries `item_id: "fc_<call_id>"`. `processResponsesStream` now registers function-call and custom-tool-call items under `item.call_id` as a secondary lookup key (alongside `item.id`/`output_index`) so identifier-deviant hosts route deltas and done events to the right block. ([#2015](https://github.com/can1357/oh-my-pi/issues/2015))
30
+ - Fixed `PI_REQ_DEBUG` response recording truncating the captured body when a streamed response was cancelled mid-flight. The response tee in `wrapResponse` could call `FileRequestDebugResponseLog.close()` from both the `cancel` callback and the resumed `pull` (which observes `done` once the source reader is cancelled); the second caller saw the handle already nulled and returned before the first caller's pending write flushed, so the `.res.log` lost the already-buffered chunk. `close()` now memoizes its flush-and-close promise so every caller awaits the same completion.
31
+
32
+ ## [15.10.0] - 2026-06-06
33
+
34
+ ### Added
35
+
36
+ - Added a dependency-free `@oh-my-pi/pi-ai/effort` module exporting the `Effort` enum and `THINKING_EFFORTS`, split out of `model-thinking` so hot-path consumers can import the thinking levels without pulling in `model-thinking` and its provider-compat dependency graph. The package barrel still re-exports both names, so existing imports are unaffected.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed Antigravity usage provider emitting one bar per model instead of deduplicating by tier — a single account's 15+ model entries now collapse to one bar per tier, matching the shared-quota reality of the upstream API.
41
+ - Fixed Antigravity usage reports missing `email` and `accountId` in metadata, so the `/usage` display and the deduplicator can associate reports with their credentials.
42
+ - Fixed usage-report dedup ignoring `projectId` for Google Cloud providers, preventing duplicate credential entries from being recognized as the same account.
43
+
44
+ - Fixed Cloud Code Assist (Antigravity / Gemini CLI) rejecting the `github` tool with HTTP 400 when the `pr` parameter schema contained `anyOf: [string, array]`. The CCA mixed-type combiner collapse picked the first non-null type (`string`) but indiscriminately copied type-specific keys from variant branches — `items` from the array variant leaked onto the string-typed result, producing `{type: "string", items: {...}}` which Google's API rejects as invalid. The collapse now filters merged variant fields against the winning type's allowed key set. ([#2002](https://github.com/can1357/oh-my-pi/pull/2002))
45
+ - Fixed OpenAI Responses-family providers (Codex, OpenAI Responses, Azure Responses) rejecting requests with `400 No tool output found for function call …` after the user branched/navigated the session tree to a node that ends on a tool call (the tool-result child is dropped from the reconstructed history) or after a turn was aborted/crashed between the call streaming and its result persisting. The converters now synthesize a placeholder `function_call_output`/`custom_tool_call_output` immediately after any unpaired `function_call`/`custom_tool_call`, symmetric to the existing orphan-output repair, so the model still sees the call and can recover instead of the whole request 400ing.
46
+ - Fixed Anthropic-compatible reasoning endpoints losing prior-turn reasoning on continuation requests when they emit unsigned `thinking` blocks. `convertAnthropicMessages` treated unknown endpoints as signature-enforcing and demoted unsigned reasoning to `type: "text"`, which destabilized tool-call argument serialization on the next turn — the upstream symptom behind the `args?.ops?.map is not a function` crash reported against the `todo` tool. Official `api.anthropic.com` keeps the conservative text fallback; non-official `anthropic-messages` reasoning models now replay unsigned reasoning as native `type: "thinking"` ([#2005](https://github.com/can1357/oh-my-pi/issues/2005)).
47
+
5
48
  ## [15.9.67] - 2026-06-06
6
49
 
7
50
  ### Fixed
@@ -1,4 +1,4 @@
1
- import type { Effort } from "../model-thinking";
1
+ import type { Effort } from "../effort";
2
2
  import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Context, ServiceTier, TokenTaskBudget } from "../types";
3
3
  /**
4
4
  * Wire types for the omp auth-gateway.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Context passed to an {@link ApiKeyResolver} on each resolution attempt.
3
+ *
4
+ * The `error`/`lastChance` pair drives the central a/b/c retry policy shared by
5
+ * the streaming ({@link streamSimple}) and non-streaming ({@link withAuth})
6
+ * drivers:
7
+ * - `error === undefined` → **initial resolve** (no force-refresh; cheap, may
8
+ * return a locally-cached not-yet-expired token).
9
+ * - `error !== undefined && !lastChance` → **step (b): refresh the SAME
10
+ * account** (force a token re-mint / await an in-flight broker refresh).
11
+ * - `error !== undefined && lastChance` → **step (c): switch account**
12
+ * (invalidate/usage-limit the current credential and rotate to a sibling).
13
+ *
14
+ * The resolver returns the bearer to send, or `undefined` to stop retrying and
15
+ * surface the last error to the caller.
16
+ */
17
+ export interface ApiKeyResolveContext {
18
+ /** True on the final retry step — the resolver should rotate to a sibling credential. */
19
+ lastChance: boolean;
20
+ /** The auth error that triggered this re-resolution, or `undefined` on the initial resolve. */
21
+ error: unknown;
22
+ /** Caller cancel signal, threaded into any credential refresh / rotation work. */
23
+ signal?: AbortSignal;
24
+ }
25
+ /**
26
+ * Resolves the API key to send for a request, retried through the a/b/c policy
27
+ * described on {@link ApiKeyResolveContext}.
28
+ */
29
+ export type ApiKeyResolver = (ctx: ApiKeyResolveContext) => Promise<string | undefined> | string | undefined;
30
+ /** A static bearer string, or a {@link ApiKeyResolver} that mints/rotates one. */
31
+ export type ApiKey = string | ApiKeyResolver;
32
+ /** Narrows {@link ApiKey} to its resolver form. */
33
+ export declare function isApiKeyResolver(key: ApiKey | undefined): key is ApiKeyResolver;
34
+ /**
35
+ * Performs the initial resolve of an {@link ApiKey} (`error: undefined`,
36
+ * `lastChance: false`). Static keys pass through unchanged.
37
+ */
38
+ export declare function resolveApiKeyOnce(key: ApiKey | undefined, signal?: AbortSignal): Promise<string | undefined>;
39
+ /**
40
+ * Classifies whether an error should trigger a credential refresh/rotation
41
+ * retry: a hard `401`, or a rotatable usage-limit ("usage_limit_reached",
42
+ * Codex's "you have hit your ChatGPT usage limit", etc.).
43
+ */
44
+ export declare function isAuthRetryableError(error: unknown): boolean;
45
+ /**
46
+ * The ordered `lastChance` values for the retry steps after the initial
47
+ * attempt fails: `false` → step (b) refresh-same, `true` → step (c) switch.
48
+ * Shared by {@link withAuth} and the streaming retry driver so both run the
49
+ * same policy.
50
+ */
51
+ export declare const AUTH_RETRY_STEPS: readonly boolean[];
52
+ /** Resolve a single retry step, swallowing resolver failures into `undefined`. */
53
+ export declare function resolveRetryKey(resolver: ApiKeyResolver, lastChance: boolean, error: unknown, signal?: AbortSignal): Promise<string | undefined>;
54
+ /**
55
+ * Runs an auth-protected operation through the central a/b/c retry policy.
56
+ *
57
+ * - A static string key (or any non-resolver) → a single `attempt` with no
58
+ * retry (identical to the legacy static-key path).
59
+ * - A resolver → initial `attempt`, then on a retryable auth error up to two
60
+ * more attempts (refresh-same, then switch). A step is skipped when the
61
+ * resolver returns the same key it just tried or `undefined`; non-auth errors
62
+ * propagate immediately.
63
+ *
64
+ * Used by non-streaming consumers (image generation, web search, completion
65
+ * helpers). The streaming driver in `stream.ts` implements the same policy with
66
+ * its replay-safe buffering machinery.
67
+ */
68
+ export declare function withAuth<T>(key: ApiKey | undefined, attempt: (key: string) => Promise<T>, opts?: {
69
+ isAuthError?: (error: unknown) => boolean;
70
+ signal?: AbortSignal;
71
+ missingKeyMessage?: string;
72
+ }): Promise<T>;
@@ -8,6 +8,7 @@
8
8
  * - `SqliteAuthCredentialStore`: concrete SQLite-backed implementation
9
9
  */
10
10
  import { Database } from "bun:sqlite";
11
+ import type { ApiKeyResolver } from "./auth-retry";
11
12
  import type { Provider } from "./types";
12
13
  import type { CredentialRankingStrategy, UsageLogger, UsageProvider, UsageReport } from "./usage";
13
14
  import type { OAuthController, OAuthCredentials, OAuthProviderId } from "./utils/oauth/types";
@@ -366,6 +367,13 @@ type AuthApiKeyOptions = {
366
367
  * stranding the caller for `timeoutMs * (maxRetries + 1)`.
367
368
  */
368
369
  signal?: AbortSignal;
370
+ /**
371
+ * Force a re-mint of the session-preferred OAuth credential's access token,
372
+ * bypassing the not-yet-expired short-circuit. Powers step (b) of the
373
+ * auth-retry policy ("refresh the SAME account") so a locally-cached token
374
+ * that a peer/broker rotated out from under us is replaced before retrying.
375
+ */
376
+ forceRefresh?: boolean;
369
377
  };
370
378
  /**
371
379
  * Refreshed OAuth access plus identity metadata returned by
@@ -647,6 +655,41 @@ export declare class AuthStorage {
647
655
  getOAuthAccesses(provider: string, options?: AuthApiKeyOptions): Promise<OAuthAccessResolution[]>;
648
656
  invalidateCredentialMatching(provider: string, apiKey: string, options?: InvalidateCredentialMatchingOptions): Promise<boolean>;
649
657
  invalidateCredentialMatching(provider: string, apiKey: string, signal?: AbortSignal): Promise<boolean>;
658
+ /**
659
+ * Rotate away from the session's current credential after a retryable auth
660
+ * error — step (c) of the auth-retry policy. Stateless: looks up the
661
+ * session-sticky credential (no API-key matching needed), applies the
662
+ * storage action for the error class, then clears the sticky so the next
663
+ * {@link AuthStorage.getApiKey} for this session picks a sibling.
664
+ *
665
+ * - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
666
+ * (temporary block via its own backoff — default plus server usage-report
667
+ * reset; sticky left intact so the next resolve re-ranks around the block).
668
+ * - otherwise (hard 401 / auth failure) → mark the credential suspect (or
669
+ * reload when no broker hook is wired) and block it, then drop the sticky.
670
+ *
671
+ * Returns whether another usable credential of the same type remains.
672
+ */
673
+ rotateSessionCredential(provider: string, sessionId: string | undefined, options?: {
674
+ error?: unknown;
675
+ signal?: AbortSignal;
676
+ }): Promise<boolean>;
677
+ /**
678
+ * Build an {@link ApiKeyResolver} backed by this storage, implementing the
679
+ * central a/b/c auth-retry policy:
680
+ *
681
+ * - initial (`error: undefined`) → resolve the session credential.
682
+ * - step (b) `!lastChance` → force-refresh the SAME session-sticky credential.
683
+ * - step (c) `lastChance` → rotate to a sibling credential, then re-resolve.
684
+ *
685
+ * Used by web-search providers and other consumers that hold an AuthStorage
686
+ * directly (no ModelRegistry in scope).
687
+ */
688
+ resolver(provider: string, options?: {
689
+ sessionId?: string;
690
+ baseUrl?: string;
691
+ modelId?: string;
692
+ }): ApiKeyResolver;
650
693
  /**
651
694
  * Build a redacted snapshot of all loaded credentials for the auth-broker
652
695
  * wire. OAuth refresh tokens are replaced with {@link REMOTE_REFRESH_SENTINEL}
@@ -0,0 +1,9 @@
1
+ /** User-facing thinking levels, ordered least to most intensive. */
2
+ export declare const enum Effort {
3
+ Minimal = "minimal",
4
+ Low = "low",
5
+ Medium = "medium",
6
+ High = "high",
7
+ XHigh = "xhigh"
8
+ }
9
+ export declare const THINKING_EFFORTS: readonly Effort[];
@@ -3,7 +3,9 @@ export * from "./api-registry";
3
3
  export * from "./auth-broker";
4
4
  export { type AuthGatewayBootOptions, type ModelResolver, startAuthGateway } from "./auth-gateway/server";
5
5
  export * from "./auth-gateway/types";
6
+ export * from "./auth-retry";
6
7
  export * from "./auth-storage";
8
+ export * from "./effort";
7
9
  export * from "./model-cache";
8
10
  export * from "./model-manager";
9
11
  export * from "./model-thinking";
@@ -1,13 +1,5 @@
1
+ import { Effort } from "./effort";
1
2
  import type { Api, Model as ApiModel } from "./types";
2
- /** User-facing thinking levels, ordered least to most intensive. */
3
- export declare const enum Effort {
4
- Minimal = "minimal",
5
- Low = "low",
6
- Medium = "medium",
7
- High = "high",
8
- XHigh = "xhigh"
9
- }
10
- export declare const THINKING_EFFORTS: readonly Effort[];
11
3
  /**
12
4
  * Static fallback model injected when Cloudflare AI Gateway discovery
13
5
  * returns no results. Ensures the provider always has at least one usable
@@ -91,10 +83,3 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
91
83
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
92
84
  */
93
85
  export declare function supportsMidConversationSystemMessages(modelId: string): boolean;
94
- /**
95
- * Claude Opus 4.8 must emit at most one tool call per turn: the Anthropic
96
- * Messages provider sends `tool_choice.disable_parallel_tool_use = true` for
97
- * this model. Scoped to exactly 4.8 — earlier and later Opus versions keep
98
- * Anthropic's default parallel tool-calling.
99
- */
100
- export declare function disablesParallelToolUse(modelId: string): boolean;
@@ -131,6 +131,11 @@ export declare function isFireworksKimiK2ModelId(modelId: string): boolean;
131
131
  * on Fireworks-backed providers, leaving every other model untouched.
132
132
  */
133
133
  export declare function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
134
+ /**
135
+ * Fireworks DeepSeek V4 accepts effort via `reasoning_effort` but rejects the
136
+ * DeepSeek-native binary `thinking` toggle when both are present.
137
+ */
138
+ export declare function stripFireworksDeepSeekThinkingToggle(model: Model<"openai-completions">, publicModelId: string): Model<"openai-completions">;
134
139
  export interface FireworksModelManagerConfig {
135
140
  apiKey?: string;
136
141
  baseUrl?: string;
@@ -6,7 +6,7 @@
6
6
  * No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
7
7
  * Bun's native `HTTPS_PROXY` support.
8
8
  */
9
- import type { Effort } from "../model-thinking";
9
+ import type { Effort } from "../effort";
10
10
  import type { StreamFunction, StreamOptions, ThinkingBudgets } from "../types";
11
11
  export type BedrockThinkingDisplay = "summarized" | "omitted";
12
12
  export interface BedrockOptions extends StreamOptions {
@@ -38,6 +38,21 @@ export declare function collectCustomCallIds(messages: ResponseInput): Set<strin
38
38
  * codex provider — issue #1351 / regression of #472.
39
39
  */
40
40
  export declare function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput;
41
+ /**
42
+ * Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
43
+ * for every `function_call` / `custom_tool_call` whose `call_id` has no matching
44
+ * output later in the same input. The Responses API rejects an unpaired call
45
+ * with `400 No tool output found for function call …`.
46
+ *
47
+ * Orphan calls surface when the user branches/navigates the session tree to a
48
+ * node that ends on a tool call (the tool-result child is excluded from the
49
+ * reconstructed history) or when a turn is aborted/crashes after the call
50
+ * streamed but before its result persisted. Dropping the call would erase the
51
+ * assistant's action; a placeholder output keeps the call visible so the model
52
+ * can recover (e.g. re-issue the call). Symmetric to
53
+ * {@link repairOrphanResponsesToolOutputs}.
54
+ */
55
+ export declare function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput;
41
56
  export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean): ResponseInputContent[] | undefined;
42
57
  export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>): ResponseInput;
43
58
  export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
@@ -1,4 +1,4 @@
1
- import type { Effort } from "./model-thinking";
1
+ import type { Effort } from "./effort";
2
2
  import type { AnthropicOptions } from "./providers/anthropic";
3
3
  import type { GoogleOptions } from "./providers/google";
4
4
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
@@ -1,4 +1,5 @@
1
1
  import type { ZodType, z } from "zod/v4";
2
+ import type { ApiKey } from "./auth-retry";
2
3
  import type { BedrockOptions } from "./providers/amazon-bedrock";
3
4
  import type { AnthropicOptions } from "./providers/anthropic";
4
5
  import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses";
@@ -50,7 +51,7 @@ export interface ThinkingConfig {
50
51
  }
51
52
  export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
52
53
  export type Provider = KnownProvider | string;
53
- import type { Effort } from "./model-thinking";
54
+ import type { Effort } from "./effort";
54
55
  /** Token budgets for each thinking level (token-based providers only) */
55
56
  export type ThinkingBudgets = {
56
57
  [key in Effort]?: number;
@@ -159,12 +160,6 @@ export interface StreamOptions {
159
160
  maxTokens?: number;
160
161
  signal?: AbortSignal;
161
162
  apiKey?: string;
162
- /**
163
- * Called when a provider returns 401 before any replay-unsafe assistant
164
- * event has been emitted. Returning a different key retries the provider
165
- * request once.
166
- */
167
- onAuthError?: (provider: string, apiKey: string, error: unknown) => Promise<string | undefined>;
168
163
  cacheRetention?: CacheRetention;
169
164
  /**
170
165
  * Additional headers to include in provider requests.
@@ -274,7 +269,15 @@ export interface StreamOptions {
274
269
  /** Cursor exec/MCP tool handlers (cursor-agent only). */
275
270
  execHandlers?: CursorExecHandlers;
276
271
  }
277
- export interface SimpleStreamOptions extends StreamOptions {
272
+ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
273
+ /**
274
+ * API key for the request: either a static bearer string, or an
275
+ * {@link ApiKeyResolver} that mints/rotates the key across the central
276
+ * a/b/c auth-retry policy. `streamSimple`/`completeSimple` resolve a
277
+ * resolver to a string before per-provider dispatch, so providers only
278
+ * ever see the resolved {@link StreamOptions.apiKey} string.
279
+ */
280
+ apiKey?: ApiKey;
278
281
  reasoning?: Effort;
279
282
  /**
280
283
  * Force-disable reasoning for the request even when the model supports it.
@@ -30,6 +30,11 @@ export declare const NON_STRUCTURAL_SCHEMA_KEYS: Record<string, true>;
30
30
  * Used when collapsing mixed-type combiner variants for CCA Claude.
31
31
  */
32
32
  export declare const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record<string, Record<string, true>>;
33
+ /**
34
+ * Flat set of every type-specific key across all CCA types.
35
+ * Used to identify sibling keys that need filtering during mixed-type collapse.
36
+ */
37
+ export declare const ALL_CCA_TYPE_SPECIFIC_KEYS: Record<string, true>;
33
38
  /**
34
39
  * Cloud Code Assist shared schema keys allowed on any type.
35
40
  * Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing.
@@ -3,6 +3,14 @@ export interface JsonSchemaValidationIssue {
3
3
  message: string;
4
4
  expectedTypes?: string[];
5
5
  keyword?: string;
6
+ /**
7
+ * Marks issues that originate inside a failed `anyOf` / `oneOf` branch.
8
+ * Consumers such as the tool-argument coercion layer use this to avoid
9
+ * applying type repairs (e.g. singleton-array wrapping) that would be
10
+ * authoritative outside of a combinator but are only one candidate
11
+ * branch's expectation here.
12
+ */
13
+ fromUnionBranch?: boolean;
6
14
  }
7
15
  export interface JsonSchemaValidationResult {
8
16
  success: boolean;
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.9.67",
4
+ "version": "15.10.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bufbuild/protobuf": "^2.12.0",
42
- "@oh-my-pi/pi-utils": "15.9.67",
42
+ "@oh-my-pi/pi-utils": "15.10.1",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -18,8 +18,9 @@
18
18
  * POST /v1/responses → OpenAI Responses in/out
19
19
  */
20
20
  import { extractRetryHint, logger } from "@oh-my-pi/pi-utils";
21
+ import type { ApiKeyResolver } from "../auth-retry";
21
22
  import type { AuthStorage } from "../auth-storage";
22
- import { Effort } from "../model-thinking";
23
+ import { Effort } from "../effort";
23
24
  import * as anthropicMessages from "../providers/anthropic-messages-server";
24
25
  import * as openaiChat from "../providers/openai-chat-server";
25
26
  import * as openaiResponses from "../providers/openai-responses-server";
@@ -340,6 +341,60 @@ async function refreshGatewayApiKeyAfterAuthError(
340
341
  return storage.getApiKey(provider, sessionId, { modelId: model.id, signal });
341
342
  }
342
343
 
344
+ /**
345
+ * Build the {@link ApiKeyResolver} handed to `streamSimple` for a gateway
346
+ * request. Drives the central a/b/c auth-retry policy server-side:
347
+ *
348
+ * - initial resolve → the credential already resolved for this request.
349
+ * - step (b) `!lastChance` → force-refresh the SAME session-sticky credential
350
+ * (a peer/broker may have rotated its token out from under our cached copy).
351
+ * - step (c) `lastChance` → {@link refreshGatewayApiKeyAfterAuthError} switches
352
+ * to a sibling (usage-limit block vs credential invalidation by error class).
353
+ *
354
+ * `lastKey` tracks the most recent bearer so the switch step invalidates the
355
+ * credential that actually failed.
356
+ */
357
+ function buildGatewayApiKeyResolver(
358
+ storage: AuthStorage,
359
+ model: Model<Api>,
360
+ sessionId: string,
361
+ initialKey: string,
362
+ requestSignal: AbortSignal,
363
+ format: string,
364
+ peer: string,
365
+ ): ApiKeyResolver {
366
+ let lastKey = initialKey;
367
+ return async ({ lastChance, error, signal }) => {
368
+ const sig = signal ?? requestSignal;
369
+ if (error === undefined) {
370
+ lastKey = initialKey;
371
+ return initialKey;
372
+ }
373
+ if (!lastChance) {
374
+ const refreshed = await storage.getApiKey(model.provider, sessionId, {
375
+ modelId: model.id,
376
+ signal: sig,
377
+ forceRefresh: true,
378
+ });
379
+ lastKey = refreshed ?? lastKey;
380
+ return refreshed;
381
+ }
382
+ const next = await refreshGatewayApiKeyAfterAuthError(
383
+ storage,
384
+ model,
385
+ sessionId,
386
+ model.provider,
387
+ lastKey,
388
+ error,
389
+ sig,
390
+ format,
391
+ peer,
392
+ );
393
+ lastKey = next ?? lastKey;
394
+ return next;
395
+ };
396
+ }
397
+
343
398
  function clientClosedResponse(route: { module: FormatModule }): Response {
344
399
  return route.module.formatError(499, "request_aborted", "client closed request");
345
400
  }
@@ -447,19 +502,15 @@ async function handleFormatEndpoint(
447
502
  }
448
503
 
449
504
  const streamOpts = buildStreamOptions(parsed, model.api, controller.signal);
450
- streamOpts.apiKey = apiKey;
451
- streamOpts.onAuthError = (provider, oldKey, error) =>
452
- refreshGatewayApiKeyAfterAuthError(
453
- bootOpts.storage,
454
- model,
455
- sessionId,
456
- provider,
457
- oldKey,
458
- error,
459
- controller.signal,
460
- route.label,
461
- peer,
462
- );
505
+ streamOpts.apiKey = buildGatewayApiKeyResolver(
506
+ bootOpts.storage,
507
+ model,
508
+ sessionId,
509
+ apiKey,
510
+ controller.signal,
511
+ route.label,
512
+ peer,
513
+ );
463
514
 
464
515
  logger.info("auth-gateway request", {
465
516
  format: route.label,
@@ -604,18 +655,15 @@ async function handlePiNative(bootOpts: AuthGatewayBootOptions, req: Request, pe
604
655
  // only inject server-controlled fields. The codex temperature/topP strip
605
656
  // matches `buildStreamOptions` — Codex rejects them with a 400.
606
657
  const streamOpts: SimpleStreamOptions = { ...parsed.options, apiKey, signal: controller.signal };
607
- streamOpts.onAuthError = (provider, oldKey, error) =>
608
- refreshGatewayApiKeyAfterAuthError(
609
- bootOpts.storage,
610
- model,
611
- sessionId,
612
- provider,
613
- oldKey,
614
- error,
615
- controller.signal,
616
- "pi-native",
617
- peer,
618
- );
658
+ streamOpts.apiKey = buildGatewayApiKeyResolver(
659
+ bootOpts.storage,
660
+ model,
661
+ sessionId,
662
+ apiKey,
663
+ controller.signal,
664
+ "pi-native",
665
+ peer,
666
+ );
619
667
  if (model.api === "openai-codex-responses") {
620
668
  delete streamOpts.temperature;
621
669
  delete streamOpts.topP;
@@ -1,4 +1,4 @@
1
- import type { Effort } from "../model-thinking";
1
+ import type { Effort } from "../effort";
2
2
  import type {
3
3
  AssistantMessage,
4
4
  AssistantMessageEventStream,