@oh-my-pi/pi-ai 15.10.6 → 15.10.8

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 (40) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/types/provider-models/google.d.ts +3 -0
  3. package/dist/types/provider-models/ollama.d.ts +2 -0
  4. package/dist/types/provider-models/openai-compat.d.ts +38 -1
  5. package/dist/types/providers/openai-responses-shared.d.ts +1 -1
  6. package/dist/types/registry/api-key-validation.d.ts +4 -0
  7. package/dist/types/registry/oauth/anthropic.d.ts +2 -1
  8. package/dist/types/registry/oauth/github-copilot.d.ts +16 -12
  9. package/dist/types/registry/oauth/types.d.ts +2 -0
  10. package/dist/types/registry/oauth/xai-oauth.d.ts +2 -1
  11. package/dist/types/registry/types.d.ts +2 -1
  12. package/dist/types/types.d.ts +20 -0
  13. package/dist/types/usage.d.ts +2 -2
  14. package/dist/types/utils/discovery/gemini.d.ts +2 -2
  15. package/dist/types/utils/discovery/openai-compatible.d.ts +2 -2
  16. package/package.json +2 -2
  17. package/src/auth-storage.ts +13 -5
  18. package/src/provider-models/google.ts +18 -1
  19. package/src/provider-models/ollama.ts +6 -4
  20. package/src/provider-models/openai-compat.ts +82 -15
  21. package/src/providers/openai-codex-responses.ts +93 -7
  22. package/src/providers/openai-completions.ts +25 -1
  23. package/src/providers/openai-responses-shared.ts +5 -2
  24. package/src/providers/transform-messages.ts +31 -17
  25. package/src/registry/api-key-login.ts +3 -0
  26. package/src/registry/api-key-validation.ts +11 -3
  27. package/src/registry/kilo.ts +3 -2
  28. package/src/registry/oauth/__tests__/xai-oauth.test.ts +4 -7
  29. package/src/registry/oauth/anthropic.ts +36 -16
  30. package/src/registry/oauth/github-copilot.ts +62 -41
  31. package/src/registry/oauth/minimax-code.ts +2 -0
  32. package/src/registry/oauth/types.ts +2 -0
  33. package/src/registry/oauth/xai-oauth.ts +16 -8
  34. package/src/registry/oauth/xiaomi.ts +9 -4
  35. package/src/registry/types.ts +2 -2
  36. package/src/types.ts +21 -0
  37. package/src/usage/google-antigravity.ts +64 -17
  38. package/src/usage.ts +2 -2
  39. package/src/utils/discovery/gemini.ts +2 -2
  40. package/src/utils/discovery/openai-compatible.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.10.8] - 2026-06-09
6
+ ### Added
7
+
8
+ - Added optional `fetch` transport override (`fetch?: FetchImpl`) to Google, Ollama, and OpenAI-compatible model-manager options so dynamic model discovery and metadata lookups can use a caller-supplied HTTP client instead of only global `fetch`
9
+ - Added optional `fetch` on OAuth controller and API-key validation/login flows so token exchange, refresh, and device/PKCE login requests can be routed through a custom `fetch` implementation
10
+ - Added optional `fetch` support to usage polling context, allowing usage providers to execute usage checks using an injected HTTP client
11
+ - Added `AssistantMessage.upstreamProvider`, capturing the upstream provider an aggregator routed the request to (OpenRouter reports it via a top-level `provider` field on every chunk, e.g. `"Anthropic"`). Surfaced from the OpenAI-completions stream alongside `responseId`.
12
+
13
+ ### Fixed
14
+
15
+ - Fixed a degenerate OpenAI Codex stream (the model emits whitespace-only `function_call_arguments.delta` frames forever — commonly seen right after a `todo` tool call) terminating the turn with an error instead of recovering. The whitespace-loop circuit-breaker now (a) stops aborting the shared per-request `AbortController` — `requestSignal` is an `AbortSignal.any` over it, so aborting latched it and made every reopen on the reused `requestSetup` impossible — and (b) drops the half-built junk tool call and replays the request from scratch, bounded by `CODEX_WHITESPACE_LOOP_RETRY_LIMIT` (2). Sampling nondeterminism usually clears the loop on a fresh attempt; once the budget is exhausted the error is surfaced as before, but without the junk tool call polluting the message.
16
+ - Capped requested output tokens at 64k (`OPENAI_MAX_OUTPUT_TOKENS`, mirroring Anthropic's `CLAUDE_CODE_MAX_OUTPUT_TOKENS`) on OpenAI-family wires with a known upstream output cap — the `openai-completions` request builder (non-OpenRouter) and the shared responses sampling helper (`openai-responses`, `azure-openai-responses`). A model's catalog `maxTokens` often tracks its context window rather than the upstream's per-request output cap, so requesting the full ceiling 400'd (e.g. `z-ai/glm-4.7` asking for 131072 output exceeded the upstream's 131072-token *total* context). Output is now `min(requested, model.maxTokens, 64000)`.
17
+ - Stopped sending `max_tokens`/`max_completion_tokens` on OpenRouter (`openrouter.ai`) completions requests. OpenRouter filters out any upstream whose advertised output cap is below the requested `max_tokens`, so a value derived from the catalog (which reflects the highest-cap provider) silently excluded lower-cap upstreams — `provider.order: ["cerebras"]` for `z-ai/glm-4.7` fell through to DeepInfra because Cerebras's ~40k output cap is below the request, while `only: ["cerebras"]` (no fallback target) bypassed the filter and worked. Omitting the field lets each upstream self-cap and keeps provider routing (`only`/`order`) honored. Kimi via OpenRouter stays exempt — it derives TPM rate limits from `max_tokens`.
18
+
19
+ ## [15.10.7] - 2026-06-08
20
+
21
+ ### Fixed
22
+
23
+ - Fixed first-party Anthropic requests returning HTTP 400 "Invalid `signature` in `thinking` block" after interrupting the model during its visible output. `transformMessages` stripped the signature from every `thinking` block of an `aborted`/`error` turn, including blocks that had already finished streaming — Anthropic delivers a block's signature at `content_block_stop` before the next block starts, so a thinking block followed by `text`/`tool_use` is fully signed. The valid signature was then replayed empty (`signature: ""`), which signature-enforcing Anthropic rejects, including when the provider is routed through an LLM gateway baseUrl. Only the single mid-stream block at the abort point is now treated as untrustworthy; completed thinking blocks keep their replayable signatures ([#2144](https://github.com/can1357/oh-my-pi/issues/2144)).
24
+ - Pinned a regression test against issue [#2123](https://github.com/can1357/oh-my-pi/issues/2123): OAuth requests to adaptive-thinking Claude Opus models (4.6+) ship a `context_management.edits[clear_thinking_20251015]` block paired with the `thinking` field, but the eager-todo prelude (and other paths that force `tool_choice` to `tool`/`any` on the first user turn) route through `disableThinkingIfToolChoiceForced`, which would strip `params.thinking` while leaving the orphan `context_management` behind. The Anthropic API then rejected the request with `400 ... clear_thinking_20251015 strategy requires thinking to be enabled or adaptive`. The fix that lands in [15.10.5] now drops both fields together; the new test locks the contract so the strategy can never outlive its enabling `thinking` payload again.
25
+ - Fixed Antigravity usage counters so exhausted Google/Gemini quota renders as `0% free` while separate Anthropic/OpenAI-backed Antigravity model counters remain visible independently, without replaying stale pre-fix cached usage reports.
26
+
5
27
  ## [15.10.6] - 2026-06-08
6
28
 
7
29
  ### Added
@@ -3080,4 +3102,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
3080
3102
 
3081
3103
  ## [0.9.4] - 2025-11-26
3082
3104
 
3083
- Initial release with multi-provider LLM support.
3105
+ Initial release with multi-provider LLM support.
@@ -2,6 +2,7 @@ import type { ModelManagerOptions } from "../model-manager";
2
2
  import type { FetchImpl } from "../types";
3
3
  export interface GoogleModelManagerConfig {
4
4
  apiKey?: string;
5
+ fetch?: FetchImpl;
5
6
  }
6
7
  export interface GoogleVertexModelManagerConfig {
7
8
  apiKey?: string;
@@ -13,10 +14,12 @@ export interface GoogleVertexModelManagerConfig {
13
14
  export interface GoogleAntigravityModelManagerConfig {
14
15
  oauthToken?: string;
15
16
  endpoint?: string;
17
+ fetch?: FetchImpl;
16
18
  }
17
19
  export interface GoogleGeminiCliModelManagerConfig {
18
20
  oauthToken?: string;
19
21
  endpoint?: string;
22
+ fetch?: FetchImpl;
20
23
  }
21
24
  export declare function googleModelManagerOptions(config?: GoogleModelManagerConfig): ModelManagerOptions<"google-generative-ai">;
22
25
  export declare function googleVertexModelManagerOptions(_config?: GoogleVertexModelManagerConfig): ModelManagerOptions;
@@ -1,7 +1,9 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
+ import type { FetchImpl } from "../types";
2
3
  export interface OllamaCloudModelManagerConfig {
3
4
  apiKey?: string;
4
5
  baseUrl?: string;
6
+ fetch?: FetchImpl;
5
7
  }
6
8
  export declare function normalizeOllamaCloudBaseUrl(baseUrl?: string): string;
7
9
  export declare function ollamaCloudModelManagerOptions(config?: OllamaCloudModelManagerConfig): ModelManagerOptions<"ollama-chat">;
@@ -1,6 +1,6 @@
1
1
  import type { ModelManagerOptions } from "../model-manager";
2
2
  import { getBundledModels } from "../models";
3
- import type { Api, Model, Provider } from "../types";
3
+ import type { Api, FetchImpl, Model, Provider } from "../types";
4
4
  export interface ModelsDevModel {
5
5
  id?: string;
6
6
  name?: string;
@@ -27,41 +27,49 @@ export interface ModelsDevModel {
27
27
  type SimpleProviderConfig = {
28
28
  apiKey?: string;
29
29
  baseUrl?: string;
30
+ fetch?: FetchImpl;
30
31
  };
31
32
  export declare function createSimpleOpenAICompletionsOptions(providerId: Parameters<typeof getBundledModels>[0], defaultBaseUrl: string, config?: SimpleProviderConfig): ModelManagerOptions<"openai-completions">;
32
33
  export interface OpenAIModelManagerConfig {
33
34
  apiKey?: string;
34
35
  baseUrl?: string;
36
+ fetch?: FetchImpl;
35
37
  }
36
38
  export declare function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses">;
37
39
  export interface GroqModelManagerConfig {
38
40
  apiKey?: string;
39
41
  baseUrl?: string;
42
+ fetch?: FetchImpl;
40
43
  }
41
44
  export declare function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelManagerOptions<"openai-completions">;
42
45
  export interface CerebrasModelManagerConfig {
43
46
  apiKey?: string;
44
47
  baseUrl?: string;
48
+ fetch?: FetchImpl;
45
49
  }
46
50
  export declare function cerebrasModelManagerOptions(config?: CerebrasModelManagerConfig): ModelManagerOptions<"openai-completions">;
47
51
  export interface HuggingfaceModelManagerConfig {
48
52
  apiKey?: string;
49
53
  baseUrl?: string;
54
+ fetch?: FetchImpl;
50
55
  }
51
56
  export declare function huggingfaceModelManagerOptions(config?: HuggingfaceModelManagerConfig): ModelManagerOptions<"openai-completions">;
52
57
  export interface NvidiaModelManagerConfig {
53
58
  apiKey?: string;
54
59
  baseUrl?: string;
60
+ fetch?: FetchImpl;
55
61
  }
56
62
  export declare function nvidiaModelManagerOptions(config?: NvidiaModelManagerConfig): ModelManagerOptions<"openai-completions">;
57
63
  export interface XaiModelManagerConfig {
58
64
  apiKey?: string;
59
65
  baseUrl?: string;
66
+ fetch?: FetchImpl;
60
67
  }
61
68
  export declare function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-completions">;
62
69
  export interface XaiOAuthModelManagerConfig {
63
70
  apiKey?: string;
64
71
  baseUrl?: string;
72
+ fetch?: FetchImpl;
65
73
  }
66
74
  interface XAICuratedModel {
67
75
  id: string;
@@ -110,16 +118,19 @@ export declare function isLikelyAimlApiChatModelId(id: string): boolean;
110
118
  export interface AimlApiModelManagerConfig {
111
119
  apiKey?: string;
112
120
  baseUrl?: string;
121
+ fetch?: FetchImpl;
113
122
  }
114
123
  export declare function aimlApiModelManagerOptions(config?: AimlApiModelManagerConfig): ModelManagerOptions<"openai-completions">;
115
124
  export interface DeepSeekModelManagerConfig {
116
125
  apiKey?: string;
117
126
  baseUrl?: string;
127
+ fetch?: FetchImpl;
118
128
  }
119
129
  export declare function deepseekModelManagerOptions(config?: DeepSeekModelManagerConfig): ModelManagerOptions<"openai-completions">;
120
130
  export interface ZhipuCodingPlanModelManagerConfig {
121
131
  apiKey?: string;
122
132
  baseUrl?: string;
133
+ fetch?: FetchImpl;
123
134
  }
124
135
  export declare function zhipuCodingPlanModelManagerOptions(config?: ZhipuCodingPlanModelManagerConfig): ModelManagerOptions<"openai-completions">;
125
136
  /**
@@ -151,11 +162,13 @@ export declare function stripFireworksDeepSeekThinkingToggle(model: Model<"opena
151
162
  export interface FireworksModelManagerConfig {
152
163
  apiKey?: string;
153
164
  baseUrl?: string;
165
+ fetch?: FetchImpl;
154
166
  }
155
167
  export declare function fireworksModelManagerOptions(config?: FireworksModelManagerConfig): ModelManagerOptions<"openai-completions">;
156
168
  export interface FirepassModelManagerConfig {
157
169
  apiKey?: string;
158
170
  baseUrl?: string;
171
+ fetch?: FetchImpl;
159
172
  }
160
173
  /**
161
174
  * Fire Pass is a Fireworks subscription product that exposes a single router
@@ -168,93 +181,111 @@ export declare function firepassModelManagerOptions(_config?: FirepassModelManag
168
181
  export interface WaferModelManagerConfig {
169
182
  apiKey?: string;
170
183
  baseUrl?: string;
184
+ fetch?: FetchImpl;
171
185
  }
172
186
  export declare function waferPassModelManagerOptions(config?: WaferModelManagerConfig): ModelManagerOptions<"openai-completions">;
173
187
  export declare function waferServerlessModelManagerOptions(config?: WaferModelManagerConfig): ModelManagerOptions<"openai-completions">;
174
188
  export interface MistralModelManagerConfig {
175
189
  apiKey?: string;
176
190
  baseUrl?: string;
191
+ fetch?: FetchImpl;
177
192
  }
178
193
  export declare function mistralModelManagerOptions(config?: MistralModelManagerConfig): ModelManagerOptions<"openai-completions">;
179
194
  export interface OpenCodeModelManagerConfig {
180
195
  apiKey?: string;
181
196
  baseUrl?: string;
197
+ fetch?: FetchImpl;
182
198
  }
183
199
  export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
184
200
  export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
185
201
  export interface OllamaModelManagerConfig {
186
202
  apiKey?: string;
187
203
  baseUrl?: string;
204
+ fetch?: FetchImpl;
188
205
  }
189
206
  export declare function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): ModelManagerOptions<"openai-responses">;
190
207
  export interface OpenRouterModelManagerConfig {
191
208
  apiKey?: string;
192
209
  baseUrl?: string;
210
+ fetch?: FetchImpl;
193
211
  }
194
212
  export declare function openrouterModelManagerOptions(config?: OpenRouterModelManagerConfig): ModelManagerOptions<"openai-completions">;
195
213
  export interface ZenMuxModelManagerConfig {
196
214
  apiKey?: string;
197
215
  baseUrl?: string;
216
+ fetch?: FetchImpl;
198
217
  }
199
218
  export declare function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): ModelManagerOptions<Api>;
200
219
  export interface KiloModelManagerConfig {
201
220
  apiKey?: string;
202
221
  baseUrl?: string;
222
+ fetch?: FetchImpl;
203
223
  }
204
224
  export declare function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelManagerOptions<"openai-completions">;
205
225
  export interface AlibabaCodingPlanModelManagerConfig {
206
226
  apiKey?: string;
207
227
  baseUrl?: string;
228
+ fetch?: FetchImpl;
208
229
  }
209
230
  export declare function alibabaCodingPlanModelManagerOptions(config?: AlibabaCodingPlanModelManagerConfig): ModelManagerOptions<"openai-completions">;
210
231
  export interface VercelAiGatewayModelManagerConfig {
211
232
  apiKey?: string;
212
233
  baseUrl?: string;
234
+ fetch?: FetchImpl;
213
235
  }
214
236
  export declare function vercelAiGatewayModelManagerOptions(config?: VercelAiGatewayModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
215
237
  export interface KimiCodeModelManagerConfig {
216
238
  apiKey?: string;
217
239
  baseUrl?: string;
240
+ fetch?: FetchImpl;
218
241
  }
219
242
  export declare function kimiCodeModelManagerOptions(config?: KimiCodeModelManagerConfig): ModelManagerOptions<"openai-completions">;
220
243
  export interface LmStudioModelManagerConfig {
221
244
  apiKey?: string;
222
245
  baseUrl?: string;
246
+ fetch?: FetchImpl;
223
247
  }
224
248
  export declare function lmStudioModelManagerOptions(config?: LmStudioModelManagerConfig): ModelManagerOptions<"openai-completions">;
225
249
  export interface SyntheticModelManagerConfig {
226
250
  apiKey?: string;
227
251
  baseUrl?: string;
252
+ fetch?: FetchImpl;
228
253
  }
229
254
  export declare function syntheticModelManagerOptions(config?: SyntheticModelManagerConfig): ModelManagerOptions<"openai-completions">;
230
255
  export interface VeniceModelManagerConfig {
231
256
  apiKey?: string;
232
257
  baseUrl?: string;
258
+ fetch?: FetchImpl;
233
259
  }
234
260
  export declare function veniceModelManagerOptions(config?: VeniceModelManagerConfig): ModelManagerOptions<"openai-completions">;
235
261
  export interface TogetherModelManagerConfig {
236
262
  apiKey?: string;
237
263
  baseUrl?: string;
264
+ fetch?: FetchImpl;
238
265
  }
239
266
  export declare function togetherModelManagerOptions(config?: TogetherModelManagerConfig): ModelManagerOptions<"openai-completions">;
240
267
  export interface MoonshotModelManagerConfig {
241
268
  apiKey?: string;
242
269
  baseUrl?: string;
270
+ fetch?: FetchImpl;
243
271
  }
244
272
  export declare function moonshotModelManagerOptions(config?: MoonshotModelManagerConfig): ModelManagerOptions<"openai-completions">;
245
273
  export interface QwenPortalModelManagerConfig {
246
274
  apiKey?: string;
247
275
  baseUrl?: string;
276
+ fetch?: FetchImpl;
248
277
  }
249
278
  export declare function qwenPortalModelManagerOptions(config?: QwenPortalModelManagerConfig): ModelManagerOptions<"openai-completions">;
250
279
  export interface QianfanModelManagerConfig {
251
280
  apiKey?: string;
252
281
  baseUrl?: string;
282
+ fetch?: FetchImpl;
253
283
  }
254
284
  export declare function qianfanModelManagerOptions(config?: QianfanModelManagerConfig): ModelManagerOptions<"openai-completions">;
255
285
  export interface CloudflareAiGatewayModelManagerConfig {
256
286
  apiKey?: string;
257
287
  baseUrl?: string;
288
+ fetch?: FetchImpl;
258
289
  }
259
290
  export declare function cloudflareAiGatewayModelManagerOptions(config?: CloudflareAiGatewayModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
260
291
  /** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
@@ -263,6 +294,7 @@ export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
263
294
  export interface XiaomiModelManagerConfig {
264
295
  apiKey?: string;
265
296
  baseUrl?: string;
297
+ fetch?: FetchImpl;
266
298
  providerId?: Provider;
267
299
  tokenPlanRegion?: XiaomiTokenPlanRegion;
268
300
  }
@@ -271,26 +303,31 @@ export declare function xiaomiModelManagerOptions(config?: XiaomiModelManagerCon
271
303
  export interface LiteLLMModelManagerConfig {
272
304
  apiKey?: string;
273
305
  baseUrl?: string;
306
+ fetch?: FetchImpl;
274
307
  }
275
308
  export declare function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig): ModelManagerOptions<"openai-completions">;
276
309
  export interface VllmModelManagerConfig {
277
310
  apiKey?: string;
278
311
  baseUrl?: string;
312
+ fetch?: FetchImpl;
279
313
  }
280
314
  export declare function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelManagerOptions<"openai-completions">;
281
315
  export interface NanoGptModelManagerConfig {
282
316
  apiKey?: string;
283
317
  baseUrl?: string;
318
+ fetch?: FetchImpl;
284
319
  }
285
320
  export declare function nanoGptModelManagerOptions(config?: NanoGptModelManagerConfig): ModelManagerOptions<"openai-completions">;
286
321
  export interface GithubCopilotModelManagerConfig {
287
322
  apiKey?: string;
288
323
  baseUrl?: string;
324
+ fetch?: FetchImpl;
289
325
  }
290
326
  export declare function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api>;
291
327
  export interface AnthropicModelManagerConfig {
292
328
  apiKey?: string;
293
329
  baseUrl?: string;
330
+ fetch?: FetchImpl;
294
331
  }
295
332
  export declare function anthropicModelManagerOptions(config?: AnthropicModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
296
333
  export { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "./discovery-constants";
@@ -85,7 +85,7 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
85
85
  * can let the upstream apply its own default instead of 400-ing on `maxTokens` values that
86
86
  * reflect the model's context window rather than the upstream output limit.
87
87
  */
88
- export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, model: Pick<Model, "provider" | "omitMaxOutputTokens">): void;
88
+ export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, model: Pick<Model, "provider" | "omitMaxOutputTokens" | "maxTokens">): void;
89
89
  type ReasoningOptions = {
90
90
  reasoning?: string;
91
91
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
@@ -1,9 +1,11 @@
1
+ import type { FetchImpl } from "../types";
1
2
  type OpenAICompatibleValidationOptions = {
2
3
  provider: string;
3
4
  apiKey: string;
4
5
  baseUrl: string;
5
6
  model: string;
6
7
  signal?: AbortSignal;
8
+ fetch?: FetchImpl;
7
9
  };
8
10
  type AnthropicCompatibleValidationOptions = {
9
11
  provider: string;
@@ -11,12 +13,14 @@ type AnthropicCompatibleValidationOptions = {
11
13
  baseUrl: string;
12
14
  model: string;
13
15
  signal?: AbortSignal;
16
+ fetch?: FetchImpl;
14
17
  };
15
18
  type ModelListValidationOptions = {
16
19
  provider: string;
17
20
  apiKey: string;
18
21
  modelsUrl: string;
19
22
  signal?: AbortSignal;
23
+ fetch?: FetchImpl;
20
24
  };
21
25
  /**
22
26
  * Validate an API key against an OpenAI-compatible chat completions endpoint.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Anthropic OAuth flow (Claude Pro/Max)
3
3
  */
4
+ import type { FetchImpl } from "../../types";
4
5
  import { OAuthCallbackFlow } from "./callback-server";
5
6
  import type { OAuthController, OAuthCredentials } from "./types";
6
7
  export declare class AnthropicOAuthFlow extends OAuthCallbackFlow {
@@ -19,4 +20,4 @@ export declare function loginAnthropic(ctrl: OAuthController): Promise<OAuthCred
19
20
  /**
20
21
  * Refresh Anthropic OAuth token
21
22
  */
22
- export declare function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials>;
23
+ export declare function refreshAnthropicToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials>;
@@ -1,8 +1,22 @@
1
+ import type { FetchImpl } from "../../types";
1
2
  import type { OAuthCredentials } from "./types";
2
3
  export declare const COPILOT_USER_AGENT: "opencode/1.3.15";
3
4
  export declare const OPENCODE_HEADERS: {
4
5
  readonly "User-Agent": "opencode/1.3.15";
5
6
  };
7
+ type GitHubCopilotLoginOptions = {
8
+ onAuth: (url: string, instructions?: string) => void;
9
+ onPrompt: (prompt: {
10
+ message: string;
11
+ placeholder?: string;
12
+ allowEmpty?: boolean;
13
+ }) => Promise<string>;
14
+ onProgress?: (message: string) => void;
15
+ signal?: AbortSignal;
16
+ pollIntervalFloorMs?: number;
17
+ pollIntervalScaleMs?: number;
18
+ fetch?: FetchImpl;
19
+ };
6
20
  export type ParsedGitHubCopilotApiKey = {
7
21
  accessToken: string;
8
22
  enterpriseUrl?: string;
@@ -24,15 +38,5 @@ export declare function refreshGitHubCopilotToken(refreshToken: string, enterpri
24
38
  * @param options.onProgress - Optional progress callback
25
39
  * @param options.signal - Optional AbortSignal for cancellation
26
40
  */
27
- export declare function loginGitHubCopilot(options: {
28
- onAuth: (url: string, instructions?: string) => void;
29
- onPrompt: (prompt: {
30
- message: string;
31
- placeholder?: string;
32
- allowEmpty?: boolean;
33
- }) => Promise<string>;
34
- onProgress?: (message: string) => void;
35
- signal?: AbortSignal;
36
- pollIntervalFloorMs?: number;
37
- pollIntervalScaleMs?: number;
38
- }): Promise<OAuthCredentials>;
41
+ export declare function loginGitHubCopilot(options: GitHubCopilotLoginOptions): Promise<OAuthCredentials>;
42
+ export {};
@@ -1,3 +1,4 @@
1
+ import type { FetchImpl } from "../../types";
1
2
  import type { OAuthProviderUnion } from "../registry";
2
3
  export type OAuthCredentials = {
3
4
  refresh: string;
@@ -30,6 +31,7 @@ export interface OAuthController {
30
31
  onManualCodeInput?(): Promise<string>;
31
32
  onPrompt?(prompt: OAuthPrompt): Promise<string>;
32
33
  signal?: AbortSignal;
34
+ fetch?: FetchImpl;
33
35
  }
34
36
  export interface OAuthLoginCallbacks extends OAuthController {
35
37
  onAuth: (info: OAuthAuthInfo) => void;
@@ -7,6 +7,7 @@
7
7
  * {@link validateXAIEndpoint}: any non-HTTPS or non-`x.ai`/`*.x.ai` host is
8
8
  * rejected on every call site, not just the first.
9
9
  */
10
+ import type { FetchImpl } from "../../types";
10
11
  import { OAuthCallbackFlow } from "./callback-server";
11
12
  import type { OAuthController, OAuthCredentials } from "./types";
12
13
  /**
@@ -57,4 +58,4 @@ export declare function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCrede
57
58
  * re-validates the cached `token_endpoint` on the refresh hot path so a
58
59
  * cached-but-poisoned endpoint cannot silently leak a refresh_token.
59
60
  */
60
- export declare function refreshXAIOAuthToken(refreshToken: string): Promise<OAuthCredentials>;
61
+ export declare function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials>;
@@ -9,12 +9,13 @@
9
9
  * line in `./registry.ts`.
10
10
  */
11
11
  import type { ModelManagerOptions } from "../model-manager";
12
- import type { Api } from "../types";
12
+ import type { Api, FetchImpl } from "../types";
13
13
  import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
14
14
  /** Config passed to a provider's runtime model-manager factory. */
15
15
  export type ModelManagerConfig = {
16
16
  apiKey?: string;
17
17
  baseUrl?: string;
18
+ fetch?: FetchImpl;
18
19
  };
19
20
  /**
20
21
  * API-key environment fallback: either a single env var name (e.g.
@@ -15,6 +15,18 @@ import type { OpenAIResponsesOptions } from "./providers/openai-responses";
15
15
  import type { KnownProviderId } from "./registry";
16
16
  import type { AssistantMessageEventStream } from "./utils/event-stream";
17
17
  export type { AssistantMessageEventStream } from "./utils/event-stream";
18
+ /**
19
+ * Ceiling on the output-token count omp requests from any OpenAI-family endpoint
20
+ * (openai-responses, azure/xai responses, and openai-completions). Mirrors
21
+ * Anthropic's {@link CLAUDE_CODE_MAX_OUTPUT_TOKENS}.
22
+ *
23
+ * Catalog `maxTokens` frequently reflects a model's context window rather than a
24
+ * given upstream's real per-request output cap. OpenRouter, for instance,
25
+ * advertises 131072 output tokens for `z-ai/glm-4.7`, but the Cerebras upstream
26
+ * only allows ~131072 tokens total — so requesting the full ceiling overflows
27
+ * with a 400. Requested output is clamped to this value (and to `model.maxTokens`).
28
+ */
29
+ export declare const OPENAI_MAX_OUTPUT_TOKENS = 64000;
18
30
  export type KnownApi = "openai-completions" | "openai-responses" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" | "google-gemini-cli" | "google-vertex" | "ollama-chat" | "cursor-agent";
19
31
  export type Api = KnownApi | (string & {});
20
32
  export interface ApiOptionsMap {
@@ -449,6 +461,14 @@ export interface AssistantMessage {
449
461
  provider: Provider;
450
462
  model: string;
451
463
  responseId?: string;
464
+ /**
465
+ * Name of the upstream provider an aggregator routed this request to, as
466
+ * reported in the response (e.g. OpenRouter's top-level `provider` field:
467
+ * `"OpenAI"`, `"Anthropic"`, `"Together"`). Distinct from `provider`, which
468
+ * is the configured gateway we called (`"openrouter"`). Undefined for direct
469
+ * providers that expose no such field.
470
+ */
471
+ upstreamProvider?: string;
452
472
  usage: Usage;
453
473
  stopReason: StopReason;
454
474
  errorMessage?: string;
@@ -5,7 +5,7 @@
5
5
  * and shared quotas across providers.
6
6
  */
7
7
  import * as z from "zod/v4";
8
- import type { Provider } from "./types";
8
+ import type { FetchImpl, Provider } from "./types";
9
9
  export type UsageUnit = "percent" | "tokens" | "requests" | "usd" | "minutes" | "bytes" | "unknown";
10
10
  export type UsageStatus = "ok" | "warning" | "exhausted" | "unknown";
11
11
  /** Time window for a limit (e.g. 5h, 7d, monthly). */
@@ -231,7 +231,7 @@ export interface UsageFetchParams {
231
231
  }
232
232
  /** Shared runtime utilities for fetchers. */
233
233
  export interface UsageFetchContext {
234
- fetch: typeof fetch;
234
+ fetch: FetchImpl;
235
235
  logger?: UsageLogger;
236
236
  retryWait?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
237
237
  }
@@ -1,4 +1,4 @@
1
- import type { Model } from "../../types";
1
+ import type { FetchImpl, Model } from "../../types";
2
2
  /**
3
3
  * Configuration for Google Generative AI model discovery.
4
4
  */
@@ -14,7 +14,7 @@ export interface GeminiDiscoveryOptions {
14
14
  /** Optional abort signal for HTTP requests. */
15
15
  signal?: AbortSignal;
16
16
  /** Optional fetch implementation override for tests. */
17
- fetch?: typeof fetch;
17
+ fetch?: FetchImpl;
18
18
  }
19
19
  /**
20
20
  * Fetches and normalizes Google Generative AI models from the public models endpoint.
@@ -1,4 +1,4 @@
1
- import type { Api, Model, Provider } from "../../types";
1
+ import type { Api, FetchImpl, Model, Provider } from "../../types";
2
2
  /**
3
3
  * Minimal OpenAI-style model entry shape consumed by discovery.
4
4
  *
@@ -51,7 +51,7 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
51
51
  /** Optional AbortSignal for request cancellation. */
52
52
  signal?: AbortSignal;
53
53
  /** Optional fetch implementation override for testing/custom runtimes. */
54
- fetch?: typeof globalThis.fetch;
54
+ fetch?: FetchImpl;
55
55
  /**
56
56
  * Optional post-normalization filter.
57
57
  * Return false to skip a model.
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.10.6",
4
+ "version": "15.10.8",
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.10.6",
42
+ "@oh-my-pi/pi-utils": "15.10.8",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -492,6 +492,9 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
492
492
  // Bumped from 3s — Claude usage retries up to 3 times with exponential backoff
493
493
  // (~3.5s total worst case); a tight per-request budget aborts retries mid-cycle.
494
494
  const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
495
+ const USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES: Partial<Record<Provider, number>> = {
496
+ "google-antigravity": 2,
497
+ };
495
498
  const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
496
499
  /**
497
500
  * Refresh OAuth access tokens this many ms before their stated expiry. The
@@ -1568,6 +1571,7 @@ export class AuthStorage {
1568
1571
  onPrompt: ctrl.onPrompt,
1569
1572
  onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
1570
1573
  signal: ctrl.signal,
1574
+ fetch: ctrl.fetch,
1571
1575
  });
1572
1576
  if (typeof result === "string") {
1573
1577
  // Some flows (e.g. ollama) return "" to signal that no key was entered.
@@ -1638,15 +1642,19 @@ export class AuthStorage {
1638
1642
  #buildUsageReportCacheKey(request: UsageRequestDescriptor): string {
1639
1643
  const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
1640
1644
  const identity = this.#buildUsageCacheIdentity(request.credential);
1641
- return `report:${request.provider}:${baseUrl}:${identity}`;
1645
+ const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
1646
+ const providerKey = versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
1647
+ return `report:${providerKey}:${baseUrl}:${identity}`;
1642
1648
  }
1643
1649
 
1644
1650
  #buildUsageReportsCacheKey(requests: ReadonlyArray<UsageRequestDescriptor>): string {
1645
1651
  const snapshot = requests
1646
- .map(
1647
- request =>
1648
- `${request.provider}:${this.#normalizeUsageBaseUrl(request.baseUrl) || "default"}:${this.#buildUsageCacheIdentity(request.credential)}`,
1649
- )
1652
+ .map(request => {
1653
+ const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
1654
+ const providerKey =
1655
+ versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
1656
+ return `${providerKey}:${this.#normalizeUsageBaseUrl(request.baseUrl) || "default"}:${this.#buildUsageCacheIdentity(request.credential)}`;
1657
+ })
1650
1658
  .sort()
1651
1659
  .join("\n");
1652
1660
  return `reports:${Bun.hash(snapshot).toString(16)}`;
@@ -5,6 +5,7 @@ import { fetchGeminiModels } from "../utils/discovery/gemini";
5
5
 
6
6
  export interface GoogleModelManagerConfig {
7
7
  apiKey?: string;
8
+ fetch?: FetchImpl;
8
9
  }
9
10
 
10
11
  export interface GoogleVertexModelManagerConfig {
@@ -18,22 +19,36 @@ export interface GoogleVertexModelManagerConfig {
18
19
  export interface GoogleAntigravityModelManagerConfig {
19
20
  oauthToken?: string;
20
21
  endpoint?: string;
22
+ fetch?: FetchImpl;
21
23
  }
22
24
 
23
25
  export interface GoogleGeminiCliModelManagerConfig {
24
26
  oauthToken?: string;
25
27
  endpoint?: string;
28
+ fetch?: FetchImpl;
26
29
  }
27
30
 
28
31
  const CLOUD_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
29
32
 
33
+ function toDiscoveryFetch(fetchImpl: FetchImpl | undefined): typeof fetch | undefined {
34
+ if (!fetchImpl) {
35
+ return undefined;
36
+ }
37
+ return Object.assign(
38
+ (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetchImpl(input, init),
39
+ { preconnect: fetchImpl.preconnect ?? fetch.preconnect },
40
+ );
41
+ }
42
+
30
43
  export function googleModelManagerOptions(
31
44
  config?: GoogleModelManagerConfig,
32
45
  ): ModelManagerOptions<"google-generative-ai"> {
33
46
  const apiKey = config?.apiKey;
34
47
  return {
35
48
  providerId: "google",
36
- ...(apiKey ? { fetchDynamicModels: () => fetchGeminiModels({ apiKey }) } : undefined),
49
+ ...(apiKey
50
+ ? { fetchDynamicModels: () => fetchGeminiModels({ apiKey, fetch: toDiscoveryFetch(config?.fetch) }) }
51
+ : undefined),
37
52
  };
38
53
  }
39
54
 
@@ -53,6 +68,7 @@ export function googleAntigravityModelManagerOptions(
53
68
  fetchAntigravityDiscoveryModels({
54
69
  token,
55
70
  endpoint: config?.endpoint,
71
+ fetcher: toDiscoveryFetch(config?.fetch),
56
72
  }),
57
73
  }
58
74
  : undefined),
@@ -72,6 +88,7 @@ export function googleGeminiCliModelManagerOptions(
72
88
  const models = await fetchAntigravityDiscoveryModels({
73
89
  token,
74
90
  endpoint,
91
+ fetcher: toDiscoveryFetch(config?.fetch),
75
92
  });
76
93
  if (models === null) {
77
94
  return null;