@oh-my-pi/pi-catalog 17.0.3 → 17.0.5
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 +19 -0
- package/dist/types/discovery/codex.d.ts +2 -2
- package/dist/types/model-cache.d.ts +5 -1
- package/dist/types/provider-models/special.d.ts +1 -0
- package/dist/types/types.d.ts +29 -8
- package/package.json +3 -3
- package/src/compat/anthropic.ts +1 -0
- package/src/compat/openai.ts +59 -26
- package/src/discovery/codex.ts +2 -2
- package/src/model-cache.ts +94 -11
- package/src/model-manager.ts +72 -5
- package/src/model-thinking.ts +15 -1
- package/src/models.json +379 -137
- package/src/provider-models/special.ts +4 -2
- package/src/types.ts +29 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.5] - 2026-07-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added an Anthropic compatibility flag to allow non-official OAuth endpoints to opt into configured Claude Code fingerprint header overrides.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed a security issue where sensitive provider-defined request headers (such as API keys or credentials) were serialized in plaintext within the model cache (models.db). The cache now omits these headers, securely invalidates older cached rows, and restores or refetches them dynamically.
|
|
14
|
+
- Fixed OpenAI Codex discovery to respect caller-supplied fetch configurations (such as proxies or custom CAs) and correctly replace stale bundled models with the authenticated account catalog.
|
|
15
|
+
- Fixed stream timeouts and retry loops during long prefills on local loopback or RFC1918 backends (such as litellm proxies fronting local servers) by applying the local stream-timeout floor to these backends.
|
|
16
|
+
- Fixed Kimi K3 models served through generic OpenAI-compatible routes exposing unsupported reasoning efforts instead of the mandatory low/high/max scale.
|
|
17
|
+
|
|
18
|
+
## [17.0.4] - 2026-07-18
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Kimi-family models now use MFJS tool schema on all hosts, including proxies like OpenRouter that forward schemas to Moonshot
|
|
23
|
+
|
|
5
24
|
## [17.0.3] - 2026-07-17
|
|
6
25
|
|
|
7
26
|
### Fixed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelSpec } from "../types.js";
|
|
1
|
+
import type { FetchImpl, ModelSpec } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Fetch options for OpenAI Codex model discovery.
|
|
4
4
|
*/
|
|
@@ -18,7 +18,7 @@ export interface CodexModelDiscoveryOptions {
|
|
|
18
18
|
/** Abort signal for network request cancellation. */
|
|
19
19
|
signal?: AbortSignal;
|
|
20
20
|
/** Optional fetch implementation override for tests. */
|
|
21
|
-
fetchFn?:
|
|
21
|
+
fetchFn?: FetchImpl;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Normalized Codex discovery response.
|
|
@@ -4,6 +4,10 @@ interface CacheEntry<TApi extends Api = Api> {
|
|
|
4
4
|
fresh: boolean;
|
|
5
5
|
authoritative: boolean;
|
|
6
6
|
updatedAt: number;
|
|
7
|
+
/** Model ids whose live headers were intentionally omitted from disk. */
|
|
8
|
+
headerOmittedModelIds: readonly string[];
|
|
9
|
+
/** Header-bearing model ids that cannot be rebuilt from the static source. */
|
|
10
|
+
unrestorableHeaderModelIds: readonly string[];
|
|
7
11
|
/**
|
|
8
12
|
* Hash of the static catalog slice that was merged into `models` when this
|
|
9
13
|
* row was written. `resolveProviderModels` compares against the current
|
|
@@ -13,5 +17,5 @@ interface CacheEntry<TApi extends Api = Api> {
|
|
|
13
17
|
staticFingerprint: string;
|
|
14
18
|
}
|
|
15
19
|
export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
|
|
16
|
-
export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string): void;
|
|
20
|
+
export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string, staticHeaderSources?: readonly Model<TApi>[]): void;
|
|
17
21
|
export {};
|
|
@@ -5,6 +5,7 @@ export interface OpenAICodexModelManagerConfig {
|
|
|
5
5
|
accessToken?: string;
|
|
6
6
|
accountId?: string;
|
|
7
7
|
clientVersion?: string;
|
|
8
|
+
fetch?: FetchImpl;
|
|
8
9
|
}
|
|
9
10
|
export declare function openaiCodexModelManagerOptions(config?: OpenAICodexModelManagerConfig): ModelManagerOptions<"openai-codex-responses">;
|
|
10
11
|
export interface CursorModelManagerConfig {
|
package/dist/types/types.d.ts
CHANGED
|
@@ -272,14 +272,29 @@ export interface OpenAICompat {
|
|
|
272
272
|
supportsStrictMode?: boolean;
|
|
273
273
|
/**
|
|
274
274
|
* Tool-schema dialect the endpoint validates `tools.function.parameters`
|
|
275
|
-
* against.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
275
|
+
* against.
|
|
276
|
+
*
|
|
277
|
+
* `"moonshot-mfjs"` triggers Moonshot Flavored JSON Schema normalization
|
|
278
|
+
* (collapse `const`→`enum`, infer `type` on bare enums, strip unsupported
|
|
279
|
+
* validators/`prefixItems`) because Moonshot/Kimi native hosts reject
|
|
280
|
+
* standard JSON Schema constructs with HTTP 400.
|
|
281
|
+
*
|
|
282
|
+
* `"grammar"` triggers grammar-sampler normalization (widen bare boolean
|
|
283
|
+
* `true`/`{}` subschemas in genuine subschema slots into a value-accepting
|
|
284
|
+
* union of primitives) because grammar-constrained backends (llama.cpp, LM
|
|
285
|
+
* Studio, vLLM) build a GBNF grammar from the JSON Schema and 400 with
|
|
286
|
+
* `Unrecognized schema: true` on a bare boolean subschema (issue #5914).
|
|
287
|
+
* Boolean `additionalProperties`/`unevaluatedProperties` are preserved — the
|
|
288
|
+
* grammar converter reads them as closed/open-object semantics, and
|
|
289
|
+
* `additionalProperties: false` pins the strict object shape.
|
|
290
|
+
*
|
|
291
|
+
* Default: auto-detected — `"moonshot-mfjs"` on Moonshot native hosts
|
|
292
|
+
* (api.moonshot.ai / api.kimi.com) and Kimi-family model ids on any host,
|
|
293
|
+
* since proxies (OpenRouter, custom gateways) forward schemas to Moonshot
|
|
294
|
+
* verbatim; `"grammar"` on local OpenAI-compatible backends. Set `"none"`
|
|
295
|
+
* to opt a host out.
|
|
281
296
|
*/
|
|
282
|
-
toolSchemaFlavor?: "moonshot-mfjs" | "none";
|
|
297
|
+
toolSchemaFlavor?: "moonshot-mfjs" | "grammar" | "none";
|
|
283
298
|
/**
|
|
284
299
|
* Stream-watchdog idle-timeout floor in ms for slow reasoning hosts.
|
|
285
300
|
* Default: auto-detected (GLM coding-plan hosts, direct DeepSeek reasoning).
|
|
@@ -377,6 +392,11 @@ export interface AnthropicCompat {
|
|
|
377
392
|
* auto-detected (Z.AI hosts).
|
|
378
393
|
*/
|
|
379
394
|
requiresToolResultId?: boolean;
|
|
395
|
+
/**
|
|
396
|
+
* Allow configured Claude Code fingerprint headers to replace generated
|
|
397
|
+
* OAuth defaults on non-official Anthropic endpoints.
|
|
398
|
+
*/
|
|
399
|
+
allowAnthropicHeaderOverrides?: boolean;
|
|
380
400
|
/**
|
|
381
401
|
* Replay unsigned `thinking` blocks from prior assistant turns as native
|
|
382
402
|
* thinking instead of demoting them to text. Official Anthropic enforces
|
|
@@ -471,6 +491,8 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
471
491
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
472
492
|
/** Provider-specific wire model-id transform applied to the base id. */
|
|
473
493
|
wireModelIdMode: "raw" | "firepass" | "fireworks" | "openrouter";
|
|
494
|
+
/** See {@link OpenAICompat.toolSchemaFlavor}. Read by both wire paths when converting tools. */
|
|
495
|
+
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
474
496
|
}
|
|
475
497
|
/**
|
|
476
498
|
* Fully-resolved chat-completions compat view: every detected default
|
|
@@ -485,7 +507,6 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<Op
|
|
|
485
507
|
thinkingKeep?: OpenAICompat["thinkingKeep"];
|
|
486
508
|
streamIdleTimeoutMs?: number;
|
|
487
509
|
toolStrictMode: ResolvedToolStrictMode;
|
|
488
|
-
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
489
510
|
/** The model sits behind Vercel AI Gateway. */
|
|
490
511
|
isVercelGatewayHost: boolean;
|
|
491
512
|
dropThinkingWhenReasoningEffort: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-catalog",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.5",
|
|
5
5
|
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@bufbuild/protobuf": "^2.12.1",
|
|
37
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
37
|
+
"@oh-my-pi/pi-utils": "17.0.5",
|
|
38
38
|
"arktype": "2.2.3",
|
|
39
39
|
"zod": "^4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.0.5",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/compat/anthropic.ts
CHANGED
|
@@ -109,6 +109,7 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
|
|
109
109
|
signingEndpoint,
|
|
110
110
|
disableStrictTools: isAzure,
|
|
111
111
|
disableAdaptiveThinking: false,
|
|
112
|
+
allowAnthropicHeaderOverrides: false,
|
|
112
113
|
supportsEagerToolInputStreaming: official,
|
|
113
114
|
// Long cache retention is only sent to the official API by default;
|
|
114
115
|
// proxies opt in explicitly via `compat.supportsLongCacheRetention: true`.
|
package/src/compat/openai.ts
CHANGED
|
@@ -154,14 +154,32 @@ const OPENCODE_WHEN_THINKING: NonNullable<OpenAICompat["whenThinking"]> = {
|
|
|
154
154
|
reasoningContentField: "reasoning_content",
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
const KIMI_K3_REASONING_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
|
|
158
|
+
minimal: "low",
|
|
159
|
+
medium: "high",
|
|
160
|
+
xhigh: "max",
|
|
161
|
+
max: "max",
|
|
162
|
+
};
|
|
163
|
+
|
|
157
164
|
const MIMO_REASONING_EFFORT_MAP: NonNullable<OpenAICompat["reasoningEffortMap"]> = {
|
|
158
165
|
minimal: "low",
|
|
159
166
|
xhigh: "high",
|
|
160
167
|
};
|
|
161
168
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
function mergeModelReasoningEffortMap(
|
|
170
|
+
compat: ResolvedOpenAISharedCompat,
|
|
171
|
+
modelId: string,
|
|
172
|
+
isMimoReasoningEffortModel: boolean,
|
|
173
|
+
): void {
|
|
174
|
+
let detected: NonNullable<OpenAICompat["reasoningEffortMap"]>;
|
|
175
|
+
if (isKimiK3ModelId(modelId)) {
|
|
176
|
+
detected = KIMI_K3_REASONING_EFFORT_MAP;
|
|
177
|
+
} else if (isMimoReasoningEffortModel) {
|
|
178
|
+
detected = MIMO_REASONING_EFFORT_MAP;
|
|
179
|
+
} else {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
compat.reasoningEffortMap = { ...detected, ...compat.reasoningEffortMap };
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
function detectStrictModeSupport(provider: string, baseUrl: string): boolean {
|
|
@@ -249,11 +267,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
249
267
|
const isKimiModel = isKimiModelId(spec.id);
|
|
250
268
|
const isMoonshotNative = modelMatchesHost(hostModel, "moonshotNative");
|
|
251
269
|
const isMoonshotKimi = isKimiModel && isMoonshotNative;
|
|
252
|
-
// Kimi K3
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const isMoonshotKimiK3 = isMoonshotKimi && isKimiK3ModelId(spec.id);
|
|
270
|
+
// Native Kimi K3 uses OpenAI-style `reasoning_effort` with mandatory
|
|
271
|
+
// low/high/max thinking, not the K2.x binary `thinking: { type }` block.
|
|
272
|
+
const isKimiK3 = isKimiK3ModelId(spec.id);
|
|
273
|
+
const isMoonshotKimiK3 = isMoonshotKimi && isKimiK3;
|
|
257
274
|
const requiresEnabledThinking = isMoonshotKimi && matchesKimiK27CodeFamily(spec);
|
|
258
275
|
const usesMoonshotKimiPreservedThinking = isMoonshotKimi && isKimiK26ModelId(spec.id);
|
|
259
276
|
const isAnthropicModel =
|
|
@@ -306,6 +323,14 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
306
323
|
const isLocalOpenAICompatBackend =
|
|
307
324
|
!PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
|
|
308
325
|
(LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));
|
|
326
|
+
// Stream-timeout floor applies to ANY loopback/RFC1918 backend, INCLUDING
|
|
327
|
+
// local proxies (litellm) excluded from `isLocalOpenAICompatBackend` above:
|
|
328
|
+
// widening the first-event/idle abort ceiling only helps a slow local
|
|
329
|
+
// upstream and never pushes an extra wire field, so the proxy carve-out (a
|
|
330
|
+
// `replayReasoningContent` safety measure) must not also strip the timeout
|
|
331
|
+
// floor. Without this, a loopback litellm fronting a cold/reprocessing
|
|
332
|
+
// llama-server aborts prefill at the 100s default and retry-loops (#4786).
|
|
333
|
+
const isLocalServingBackend = isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl);
|
|
309
334
|
|
|
310
335
|
const useMaxTokens =
|
|
311
336
|
isMistral ||
|
|
@@ -378,7 +403,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
378
403
|
? KIMI_REASONING_STREAM_IDLE_TIMEOUT_MS
|
|
379
404
|
: spec.reasoning && isDirectDeepseekApi
|
|
380
405
|
? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
|
|
381
|
-
:
|
|
406
|
+
: isLocalServingBackend
|
|
382
407
|
? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
|
|
383
408
|
: undefined;
|
|
384
409
|
|
|
@@ -423,7 +448,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
423
448
|
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
|
424
449
|
// temperature/top_p/… with a 400 on every serving host (#5606).
|
|
425
450
|
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
|
|
426
|
-
reasoningEffortMap:
|
|
451
|
+
reasoningEffortMap: {},
|
|
427
452
|
supportsUsageInStreaming: !isCerebras,
|
|
428
453
|
// pi-ai's thinking-loop guard is gemini-only; default the flag from the
|
|
429
454
|
// family classifier so OpenAI-compat proxies serving Gemini are covered.
|
|
@@ -436,11 +461,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
436
461
|
// every call since the family can otherwise emit very long reasoning traces
|
|
437
462
|
// before the final answer.
|
|
438
463
|
alwaysSendMaxTokens: isKimiModel,
|
|
439
|
-
// Native Kimi K3 always reasons
|
|
464
|
+
// Native Kimi K3 always reasons through `reasoning_effort` (never the
|
|
440
465
|
// K2.x binary `thinking` block that #827's forced-tool-choice conflict is
|
|
441
|
-
// about), so suppressing its effort would
|
|
442
|
-
// normal forced-tool turns (e.g. plan-mode `toolChoice: "required"`) and
|
|
443
|
-
// leave K3 in an unsupported mode (#5758 review).
|
|
466
|
+
// about), so suppressing its effort would leave K3 in an unsupported mode.
|
|
444
467
|
disableReasoningOnForcedToolChoice: (isKimiModel && !isMoonshotKimiK3) || isAnthropicModel,
|
|
445
468
|
disableReasoningOnToolChoice: isDeepseekFamily && Boolean(spec.reasoning) && !isOpenRouter,
|
|
446
469
|
supportsToolChoice: !isDirectDeepseekReasoning,
|
|
@@ -451,11 +474,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
451
474
|
requiresAssistantAfterToolResult: isMistral,
|
|
452
475
|
requiresThinkingAsText: isMistral,
|
|
453
476
|
requiresMistralToolIds: isMistral,
|
|
454
|
-
// Only Kimi's native hosts (Moonshot / Kimi-code, matched by
|
|
455
|
-
// speak the z.ai binary `thinking: { type }` field.
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// (low|medium|high|xhigh|max|none), so those stay on the "openai" path.
|
|
477
|
+
// Only Kimi's native K2.x hosts (Moonshot / Kimi-code, matched by
|
|
478
|
+
// `isMoonshotKimi`) speak the z.ai binary `thinking: { type }` field.
|
|
479
|
+
// K3 and Kimi reached through OpenAI-compatible proxies drive reasoning
|
|
480
|
+
// via OpenAI-style `reasoning_effort`.
|
|
459
481
|
// NVIDIA NIM hosts Qwen with the vLLM convention
|
|
460
482
|
// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`
|
|
461
483
|
// is rejected by NIM's `additionalProperties: false` request schema
|
|
@@ -532,7 +554,11 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
532
554
|
supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
|
|
533
555
|
extraBody: isDirectDeepseekReasoning ? { thinking: { type: "enabled" } } : undefined,
|
|
534
556
|
toolStrictMode: isCerebras ? "all_strict" : "mixed",
|
|
535
|
-
|
|
557
|
+
// Kimi-family ids trigger MFJS on any host, not just native base URLs:
|
|
558
|
+
// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`
|
|
559
|
+
// to Moonshot verbatim, which 400s on non-MFJS constructs.
|
|
560
|
+
toolSchemaFlavor:
|
|
561
|
+
isMoonshotNative || isKimiModel ? "moonshot-mfjs" : isLocalOpenAICompatBackend ? "grammar" : undefined,
|
|
536
562
|
streamIdleTimeoutMs,
|
|
537
563
|
stripDeepseekSpecialTokens:
|
|
538
564
|
isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
|
|
@@ -554,7 +580,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
554
580
|
if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
|
|
555
581
|
compat.omitReasoningEffort = true;
|
|
556
582
|
}
|
|
557
|
-
|
|
583
|
+
mergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);
|
|
558
584
|
|
|
559
585
|
const whenThinkingPolicy =
|
|
560
586
|
spec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);
|
|
@@ -567,7 +593,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
567
593
|
if (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {
|
|
568
594
|
variant.omitReasoningEffort = true;
|
|
569
595
|
}
|
|
570
|
-
|
|
596
|
+
mergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);
|
|
571
597
|
compat.whenThinking = variant;
|
|
572
598
|
}
|
|
573
599
|
|
|
@@ -603,9 +629,13 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
603
629
|
const isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;
|
|
604
630
|
const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
|
|
605
631
|
const reasoningCapable = Boolean(spec.reasoning);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
632
|
+
// `replayReasoningContent` is Responses-only-false, so the proxy carve-out is
|
|
633
|
+
// irrelevant here; the stream-timeout floor still applies to ANY loopback /
|
|
634
|
+
// RFC1918 backend, including local proxies (litellm), so a slow local
|
|
635
|
+
// upstream is not aborted at the 100s default and retry-looped (#4786).
|
|
636
|
+
const isLocalServingBackend =
|
|
637
|
+
(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||
|
|
638
|
+
hasLocalLoopbackBaseUrl(baseUrl);
|
|
609
639
|
|
|
610
640
|
const compat: ResolvedOpenAIResponsesCompat = {
|
|
611
641
|
supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
|
|
@@ -658,6 +688,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
658
688
|
openRouterRouting: undefined,
|
|
659
689
|
isOpenRouterHost: isOpenRouter,
|
|
660
690
|
wireModelIdMode: isOpenRouter ? "openrouter" : "raw",
|
|
691
|
+
// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still
|
|
692
|
+
// lands on Moonshot's MFJS validator.
|
|
693
|
+
toolSchemaFlavor: isKimiModel ? "moonshot-mfjs" : undefined,
|
|
661
694
|
alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
|
|
662
695
|
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
|
|
663
696
|
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
|
@@ -669,7 +702,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
669
702
|
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
|
670
703
|
usesOpenAIToolCallIdLimit: spec.provider === "openai",
|
|
671
704
|
promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
|
|
672
|
-
streamIdleTimeoutMs:
|
|
705
|
+
streamIdleTimeoutMs: isLocalServingBackend
|
|
673
706
|
? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
|
|
674
707
|
: spec.compat?.streamIdleTimeoutMs,
|
|
675
708
|
};
|
package/src/discovery/codex.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
2
|
import { parseKnownModel, semverEqual } from "../identity/classify";
|
|
3
|
-
import type { ModelSpec } from "../types";
|
|
3
|
+
import type { FetchImpl, ModelSpec } from "../types";
|
|
4
4
|
import { discoveryFetch } from "../utils";
|
|
5
5
|
import { CODEX_BASE_URL, CODEX_CLIENT_VERSION, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../wire/codex";
|
|
6
6
|
|
|
@@ -69,7 +69,7 @@ export interface CodexModelDiscoveryOptions {
|
|
|
69
69
|
/** Abort signal for network request cancellation. */
|
|
70
70
|
signal?: AbortSignal;
|
|
71
71
|
/** Optional fetch implementation override for tests. */
|
|
72
|
-
fetchFn?:
|
|
72
|
+
fetchFn?: FetchImpl;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
package/src/model-cache.ts
CHANGED
|
@@ -7,16 +7,20 @@ import { getModelDbPath } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import type { Api, Model, ModelSpec } from "./types";
|
|
8
8
|
|
|
9
9
|
// Rows persist ModelSpec JSON (sparse `compat`, never the resolved record);
|
|
10
|
-
// the model manager rebuilds via `buildModel` on load.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
10
|
+
// the model manager rebuilds via `buildModel` on load. Request headers are
|
|
11
|
+
// intentionally omitted: arbitrary provider-defined header names can carry
|
|
12
|
+
// credentials. v10 deletes rows that may contain persisted headers and records
|
|
13
|
+
// which model ids lost headers and which cannot be rebuilt from static inputs,
|
|
14
|
+
// so the manager can restore the safe subset or refetch dynamic-only headers;
|
|
15
|
+
// v9 invalidated Kimi Code rows predating live effort and protocol metadata;
|
|
16
|
+
// v8 invalidated Codex discovery rows predating provider-native V2 compaction
|
|
17
|
+
// metadata; v7 invalidated rows predating the Antigravity Gemini budget-mode
|
|
18
|
+
// migration (cached specs still carrying `thinking.mode: "google-level"` and
|
|
19
|
+
// the old 3.5-flash effort routing); v6 invalidated rows that may contain the
|
|
20
|
+
// retired unknown-limit sentinels (222222/8888); v5 invalidated rows predating
|
|
17
21
|
// effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
|
|
18
22
|
// v4 dropped the pre-efforts ThinkingConfig shape.
|
|
19
|
-
const CACHE_SCHEMA_VERSION =
|
|
23
|
+
const CACHE_SCHEMA_VERSION = 10;
|
|
20
24
|
|
|
21
25
|
interface CacheRow {
|
|
22
26
|
provider_id: string;
|
|
@@ -25,6 +29,8 @@ interface CacheRow {
|
|
|
25
29
|
authoritative: number;
|
|
26
30
|
static_fingerprint: string;
|
|
27
31
|
models: string;
|
|
32
|
+
header_omitted_model_ids: string;
|
|
33
|
+
unrestorable_header_model_ids: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
interface TableInfoRow {
|
|
@@ -36,6 +42,10 @@ interface CacheEntry<TApi extends Api = Api> {
|
|
|
36
42
|
fresh: boolean;
|
|
37
43
|
authoritative: boolean;
|
|
38
44
|
updatedAt: number;
|
|
45
|
+
/** Model ids whose live headers were intentionally omitted from disk. */
|
|
46
|
+
headerOmittedModelIds: readonly string[];
|
|
47
|
+
/** Header-bearing model ids that cannot be rebuilt from the static source. */
|
|
48
|
+
unrestorableHeaderModelIds: readonly string[];
|
|
39
49
|
/**
|
|
40
50
|
* Hash of the static catalog slice that was merged into `models` when this
|
|
41
51
|
* row was written. `resolveProviderModels` compares against the current
|
|
@@ -53,6 +63,10 @@ function openDb(resolvedPath: string): Database {
|
|
|
53
63
|
// Install the busy handler BEFORE any lock-taking statement. See
|
|
54
64
|
// https://github.com/can1357/oh-my-pi/issues/2421.
|
|
55
65
|
db.run("PRAGMA busy_timeout = 3000");
|
|
66
|
+
// Schema invalidation can delete rows containing credentials written by old
|
|
67
|
+
// versions. Overwrite deleted SQLite cells instead of leaving their bytes in
|
|
68
|
+
// free pages where a raw scan of models.db can still recover them (#5780).
|
|
69
|
+
db.run("PRAGMA secure_delete = ON");
|
|
56
70
|
db.run("PRAGMA journal_mode = WAL");
|
|
57
71
|
db.run(`
|
|
58
72
|
CREATE TABLE IF NOT EXISTS model_cache (
|
|
@@ -61,6 +75,8 @@ function openDb(resolvedPath: string): Database {
|
|
|
61
75
|
updated_at INTEGER NOT NULL,
|
|
62
76
|
authoritative INTEGER NOT NULL DEFAULT 0,
|
|
63
77
|
static_fingerprint TEXT NOT NULL DEFAULT '',
|
|
78
|
+
header_omitted_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
79
|
+
unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]',
|
|
64
80
|
models TEXT NOT NULL
|
|
65
81
|
)
|
|
66
82
|
`);
|
|
@@ -99,6 +115,12 @@ function migrateCacheSchema(db: Database): void {
|
|
|
99
115
|
if (!columns.some(column => column.name === "static_fingerprint")) {
|
|
100
116
|
db.run("ALTER TABLE model_cache ADD COLUMN static_fingerprint TEXT NOT NULL DEFAULT ''");
|
|
101
117
|
}
|
|
118
|
+
if (!columns.some(column => column.name === "header_omitted_model_ids")) {
|
|
119
|
+
db.run("ALTER TABLE model_cache ADD COLUMN header_omitted_model_ids TEXT NOT NULL DEFAULT '[]'");
|
|
120
|
+
}
|
|
121
|
+
if (!columns.some(column => column.name === "unrestorable_header_model_ids")) {
|
|
122
|
+
db.run("ALTER TABLE model_cache ADD COLUMN unrestorable_header_model_ids TEXT NOT NULL DEFAULT '[]'");
|
|
123
|
+
}
|
|
102
124
|
} finally {
|
|
103
125
|
stmt.finalize();
|
|
104
126
|
}
|
|
@@ -125,6 +147,14 @@ export function readModelCache<TApi extends Api>(
|
|
|
125
147
|
return null;
|
|
126
148
|
}
|
|
127
149
|
const models = JSON.parse(row.models) as ModelSpec<TApi>[];
|
|
150
|
+
const parsedHeaderModelIds: unknown = JSON.parse(row.header_omitted_model_ids);
|
|
151
|
+
const headerOmittedModelIds = Array.isArray(parsedHeaderModelIds)
|
|
152
|
+
? parsedHeaderModelIds.filter((id): id is string => typeof id === "string")
|
|
153
|
+
: [];
|
|
154
|
+
const parsedUnrestorableModelIds: unknown = JSON.parse(row.unrestorable_header_model_ids);
|
|
155
|
+
const unrestorableHeaderModelIds = Array.isArray(parsedUnrestorableModelIds)
|
|
156
|
+
? parsedUnrestorableModelIds.filter((id): id is string => typeof id === "string")
|
|
157
|
+
: [];
|
|
128
158
|
const ageMs = now() - row.updated_at;
|
|
129
159
|
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
130
160
|
return {
|
|
@@ -132,6 +162,8 @@ export function readModelCache<TApi extends Api>(
|
|
|
132
162
|
fresh,
|
|
133
163
|
authoritative: row.authoritative === 1,
|
|
134
164
|
updatedAt: row.updated_at,
|
|
165
|
+
headerOmittedModelIds,
|
|
166
|
+
unrestorableHeaderModelIds,
|
|
135
167
|
staticFingerprint: row.static_fingerprint ?? "",
|
|
136
168
|
};
|
|
137
169
|
} finally {
|
|
@@ -143,6 +175,39 @@ export function readModelCache<TApi extends Api>(
|
|
|
143
175
|
}
|
|
144
176
|
}
|
|
145
177
|
|
|
178
|
+
/** Whether a live model carries at least one request header. */
|
|
179
|
+
function hasModelHeaders(model: Model<Api>): boolean {
|
|
180
|
+
const headers = model.headers;
|
|
181
|
+
if (!headers) return false;
|
|
182
|
+
for (const _key in headers) return true;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Project a live model to cache-safe metadata.
|
|
188
|
+
*
|
|
189
|
+
* Headers are never persisted: custom/runtime providers may use arbitrary
|
|
190
|
+
* credential header names, so no name-based filter can be complete. The
|
|
191
|
+
* separately persisted model-id list lets the manager restore matching static
|
|
192
|
+
* headers and reject/refetch dynamic-only cached models that need live headers.
|
|
193
|
+
*/
|
|
194
|
+
function toCachedModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
|
|
195
|
+
const { headers: _headers, compatConfig, ...rest } = model;
|
|
196
|
+
return { ...rest, compat: compatConfig };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Whether two in-memory header records are byte-for-byte equivalent. */
|
|
200
|
+
function headersEqual(left: Record<string, string> | undefined, right: Record<string, string> | undefined): boolean {
|
|
201
|
+
if (!left || !right) return left === right;
|
|
202
|
+
for (const key in left) {
|
|
203
|
+
if (right[key] !== left[key]) return false;
|
|
204
|
+
}
|
|
205
|
+
for (const key in right) {
|
|
206
|
+
if (!(key in left)) return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
146
211
|
export function writeModelCache<TApi extends Api>(
|
|
147
212
|
providerId: string,
|
|
148
213
|
updatedAt: number,
|
|
@@ -150,19 +215,37 @@ export function writeModelCache<TApi extends Api>(
|
|
|
150
215
|
authoritative: boolean,
|
|
151
216
|
staticFingerprint: string,
|
|
152
217
|
dbPath?: string,
|
|
218
|
+
staticHeaderSources: readonly Model<TApi>[] = [],
|
|
153
219
|
): void {
|
|
154
220
|
try {
|
|
155
221
|
withModelCacheDb(dbPath, db => {
|
|
222
|
+
const headerOmittedModelIds: string[] = [];
|
|
223
|
+
const unrestorableHeaderModelIds: string[] = [];
|
|
224
|
+
const cachedModels: ModelSpec<TApi>[] = [];
|
|
225
|
+
const staticById = new Map(staticHeaderSources.map(model => [model.id, model]));
|
|
226
|
+
for (const model of models) {
|
|
227
|
+
if (hasModelHeaders(model)) {
|
|
228
|
+
headerOmittedModelIds.push(model.id);
|
|
229
|
+
if (!headersEqual(model.headers, staticById.get(model.id)?.headers)) {
|
|
230
|
+
unrestorableHeaderModelIds.push(model.id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
cachedModels.push(toCachedModelSpec(model));
|
|
234
|
+
}
|
|
156
235
|
db.run(
|
|
157
|
-
`INSERT OR REPLACE INTO model_cache (
|
|
158
|
-
|
|
236
|
+
`INSERT OR REPLACE INTO model_cache (
|
|
237
|
+
provider_id, version, updated_at, authoritative, static_fingerprint,
|
|
238
|
+
header_omitted_model_ids, unrestorable_header_model_ids, models
|
|
239
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
159
240
|
[
|
|
160
241
|
providerId,
|
|
161
242
|
CACHE_SCHEMA_VERSION,
|
|
162
243
|
updatedAt,
|
|
163
244
|
authoritative ? 1 : 0,
|
|
164
245
|
staticFingerprint,
|
|
165
|
-
JSON.stringify(
|
|
246
|
+
JSON.stringify(headerOmittedModelIds),
|
|
247
|
+
JSON.stringify(unrestorableHeaderModelIds),
|
|
248
|
+
JSON.stringify(cachedModels),
|
|
166
249
|
],
|
|
167
250
|
);
|
|
168
251
|
});
|