@oh-my-pi/pi-catalog 17.0.4 → 17.0.6
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 -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 +27 -9
- package/package.json +3 -3
- package/src/compat/anthropic.ts +1 -0
- package/src/compat/openai.ts +53 -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 +409 -222
- package/src/provider-models/special.ts +4 -2
- package/src/types.ts +27 -9
- package/src/variant-collapse.ts +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.6] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added static fallback seed for Devin's `swe-1-7` model so it is bundled even when catalog generation runs without a Devin session token.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Collapsed Devin's six GLM-5.2 variants into two logical entries (`glm-5-2` for 200K free, `glm-5-2-1m` for 1M paid). The 200K entry routes every thinking effort to the free `glm-5-2` wire UID — never to the quota-gated `glm-5-2-max` or `glm-5-2-none` — so GLM-5.2 works even when the weekly usage quota is exhausted.
|
|
14
|
+
|
|
15
|
+
## [17.0.5] - 2026-07-18
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added an Anthropic compatibility flag to allow non-official OAuth endpoints to opt into configured Claude Code fingerprint header overrides.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- Fixed Kimi K3 models served through generic OpenAI-compatible routes exposing unsupported reasoning efforts instead of the mandatory low/high/max scale.
|
|
27
|
+
|
|
5
28
|
## [17.0.4] - 2026-07-18
|
|
6
29
|
|
|
7
30
|
### Changed
|
|
@@ -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,16 +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
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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.
|
|
283
296
|
*/
|
|
284
|
-
toolSchemaFlavor?: "moonshot-mfjs" | "none";
|
|
297
|
+
toolSchemaFlavor?: "moonshot-mfjs" | "grammar" | "none";
|
|
285
298
|
/**
|
|
286
299
|
* Stream-watchdog idle-timeout floor in ms for slow reasoning hosts.
|
|
287
300
|
* Default: auto-detected (GLM coding-plan hosts, direct DeepSeek reasoning).
|
|
@@ -379,6 +392,11 @@ export interface AnthropicCompat {
|
|
|
379
392
|
* auto-detected (Z.AI hosts).
|
|
380
393
|
*/
|
|
381
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;
|
|
382
400
|
/**
|
|
383
401
|
* Replay unsigned `thinking` blocks from prior assistant turns as native
|
|
384
402
|
* thinking instead of demoting them to text. Official Anthropic enforces
|
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.6",
|
|
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.6",
|
|
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.6",
|
|
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
|
|
@@ -535,7 +557,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
535
557
|
// Kimi-family ids trigger MFJS on any host, not just native base URLs:
|
|
536
558
|
// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`
|
|
537
559
|
// to Moonshot verbatim, which 400s on non-MFJS constructs.
|
|
538
|
-
toolSchemaFlavor:
|
|
560
|
+
toolSchemaFlavor:
|
|
561
|
+
isMoonshotNative || isKimiModel ? "moonshot-mfjs" : isLocalOpenAICompatBackend ? "grammar" : undefined,
|
|
539
562
|
streamIdleTimeoutMs,
|
|
540
563
|
stripDeepseekSpecialTokens:
|
|
541
564
|
isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
|
|
@@ -557,7 +580,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
557
580
|
if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
|
|
558
581
|
compat.omitReasoningEffort = true;
|
|
559
582
|
}
|
|
560
|
-
|
|
583
|
+
mergeModelReasoningEffortMap(compat, spec.id, isMimoReasoningEffortModel);
|
|
561
584
|
|
|
562
585
|
const whenThinkingPolicy =
|
|
563
586
|
spec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);
|
|
@@ -570,7 +593,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
570
593
|
if (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {
|
|
571
594
|
variant.omitReasoningEffort = true;
|
|
572
595
|
}
|
|
573
|
-
|
|
596
|
+
mergeModelReasoningEffortMap(variant, spec.id, isMimoReasoningEffortModel);
|
|
574
597
|
compat.whenThinking = variant;
|
|
575
598
|
}
|
|
576
599
|
|
|
@@ -606,9 +629,13 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
606
629
|
const isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;
|
|
607
630
|
const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
|
|
608
631
|
const reasoningCapable = Boolean(spec.reasoning);
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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);
|
|
612
639
|
|
|
613
640
|
const compat: ResolvedOpenAIResponsesCompat = {
|
|
614
641
|
supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
|
|
@@ -675,7 +702,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
675
702
|
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
|
676
703
|
usesOpenAIToolCallIdLimit: spec.provider === "openai",
|
|
677
704
|
promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
|
|
678
|
-
streamIdleTimeoutMs:
|
|
705
|
+
streamIdleTimeoutMs: isLocalServingBackend
|
|
679
706
|
? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
|
|
680
707
|
: spec.compat?.streamIdleTimeoutMs,
|
|
681
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
|
});
|