@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.
- package/CHANGELOG.md +23 -1
- package/dist/types/provider-models/google.d.ts +3 -0
- package/dist/types/provider-models/ollama.d.ts +2 -0
- package/dist/types/provider-models/openai-compat.d.ts +38 -1
- package/dist/types/providers/openai-responses-shared.d.ts +1 -1
- package/dist/types/registry/api-key-validation.d.ts +4 -0
- package/dist/types/registry/oauth/anthropic.d.ts +2 -1
- package/dist/types/registry/oauth/github-copilot.d.ts +16 -12
- package/dist/types/registry/oauth/types.d.ts +2 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +2 -1
- package/dist/types/registry/types.d.ts +2 -1
- package/dist/types/types.d.ts +20 -0
- package/dist/types/usage.d.ts +2 -2
- package/dist/types/utils/discovery/gemini.d.ts +2 -2
- package/dist/types/utils/discovery/openai-compatible.d.ts +2 -2
- package/package.json +2 -2
- package/src/auth-storage.ts +13 -5
- package/src/provider-models/google.ts +18 -1
- package/src/provider-models/ollama.ts +6 -4
- package/src/provider-models/openai-compat.ts +82 -15
- package/src/providers/openai-codex-responses.ts +93 -7
- package/src/providers/openai-completions.ts +25 -1
- package/src/providers/openai-responses-shared.ts +5 -2
- package/src/providers/transform-messages.ts +31 -17
- package/src/registry/api-key-login.ts +3 -0
- package/src/registry/api-key-validation.ts +11 -3
- package/src/registry/kilo.ts +3 -2
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +4 -7
- package/src/registry/oauth/anthropic.ts +36 -16
- package/src/registry/oauth/github-copilot.ts +62 -41
- package/src/registry/oauth/minimax-code.ts +2 -0
- package/src/registry/oauth/types.ts +2 -0
- package/src/registry/oauth/xai-oauth.ts +16 -8
- package/src/registry/oauth/xiaomi.ts +9 -4
- package/src/registry/types.ts +2 -2
- package/src/types.ts +21 -0
- package/src/usage/google-antigravity.ts +64 -17
- package/src/usage.ts +2 -2
- package/src/utils/discovery/gemini.ts +2 -2
- 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
|
-
|
|
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.
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -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:
|
|
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?:
|
|
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?:
|
|
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.
|
|
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.
|
|
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"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|