@oh-my-pi/pi-catalog 16.2.12 → 16.3.0
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 +26 -1
- package/dist/types/compat/openai.d.ts +2 -2
- package/dist/types/hosts.d.ts +5 -0
- package/dist/types/identity/family.d.ts +9 -0
- package/dist/types/provider-models/descriptors.d.ts +1 -1
- package/dist/types/types.d.ts +9 -16
- package/dist/types/utils.d.ts +6 -0
- package/dist/types/wire/coreweave.d.ts +1 -0
- package/package.json +3 -3
- package/src/compat/anthropic.ts +16 -9
- package/src/compat/openai.ts +29 -10
- package/src/discovery/antigravity.ts +2 -2
- package/src/discovery/codex.ts +10 -3
- package/src/discovery/devin.ts +2 -1
- package/src/discovery/gemini.ts +2 -1
- package/src/discovery/gitlab-duo-workflow.ts +5 -5
- package/src/discovery/openai-compatible.ts +2 -1
- package/src/hosts.ts +2 -0
- package/src/identity/family.ts +18 -5
- package/src/model-cache.ts +15 -9
- package/src/provider-models/descriptors.ts +1 -1
- package/src/provider-models/ollama.ts +3 -2
- package/src/provider-models/openai-compat.ts +42 -10
- package/src/types.ts +11 -18
- package/src/utils.ts +10 -0
- package/src/wire/coreweave.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.0] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `"juice-zero-developer-message"` member from `OpenAIReasoningDisableMode`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.
|
|
14
|
+
- Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.
|
|
15
|
+
- Fixed model discovery probes (including Ollama and metadata fetches) failing behind private-CA gateways by ensuring they honor the `NODE_EXTRA_CA_CERTS` environment variable.
|
|
16
|
+
- Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.
|
|
17
|
+
- Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.
|
|
18
|
+
- Fixed ZenMux's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`), matching the GitHub Copilot fix (#2851). ZenMux's `zenmux.ai/api/anthropic` route forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: ""` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused `400 messages.1.content.0: Invalid signature in thinking` on Claude Sonnet 5 and other reasoning models. ([#4192](https://github.com/can1357/oh-my-pi/issues/4192))
|
|
19
|
+
|
|
20
|
+
## [16.2.13] - 2026-07-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Added support for human-readable reasoning summaries on compatible OpenAI Codex models (v5.4+)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed discovered OpenAI Codex models to advertise V2 streaming remote compaction, avoiding the legacy compact endpoint timeout path for Codex sessions. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146))
|
|
29
|
+
|
|
5
30
|
## [16.2.12] - 2026-07-01
|
|
6
31
|
|
|
7
32
|
### Breaking Changes
|
|
@@ -174,7 +199,7 @@
|
|
|
174
199
|
|
|
175
200
|
### Fixed
|
|
176
201
|
|
|
177
|
-
- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families
|
|
202
|
+
- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067))
|
|
178
203
|
|
|
179
204
|
## [16.1.3] - 2026-06-19
|
|
180
205
|
|
|
@@ -20,8 +20,8 @@ interface OpenAIResponsesSpecLike {
|
|
|
20
20
|
* as well as URL — bundled `azure` models carry no baseUrl (the deployment host
|
|
21
21
|
* is per-resource, resolved at runtime) — while OpenAI/Copilot developer-role
|
|
22
22
|
* and prompt-cache detection stay URL-keyed, as the historical call sites were.
|
|
23
|
-
* The GPT-5
|
|
24
|
-
* request-time check.
|
|
23
|
+
* The GPT-5 reasoning-suppression prompt keys on the model name, matching the
|
|
24
|
+
* historical request-time check.
|
|
25
25
|
*/
|
|
26
26
|
export declare function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat;
|
|
27
27
|
export declare function buildOpenRouterCompat(spec: ModelSpec<"openrouter">): ResolvedOpenRouterCompat;
|
package/dist/types/hosts.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export declare const KNOWN_HOSTS: {
|
|
|
115
115
|
readonly providers: readonly ["opencode-go", "opencode-zen"];
|
|
116
116
|
readonly urlMarkers: readonly ["opencode.ai"];
|
|
117
117
|
};
|
|
118
|
+
/** ZenMux's Anthropic-compatible proxy (`zenmux.ai/api/anthropic`) forwards to signature-enforcing Anthropic. */
|
|
119
|
+
readonly zenmux: {
|
|
120
|
+
readonly providers: readonly ["zenmux"];
|
|
121
|
+
readonly urlMarkers: readonly ["zenmux.ai"];
|
|
122
|
+
};
|
|
118
123
|
readonly chutes: {
|
|
119
124
|
readonly urlMarkers: readonly ["chutes.ai"];
|
|
120
125
|
};
|
|
@@ -60,6 +60,15 @@ export declare const isOpenAIModelId: (modelId: string) => boolean;
|
|
|
60
60
|
* fall back to omitting `context`, letting the server default to `current_turn`.
|
|
61
61
|
*/
|
|
62
62
|
export declare const supportsAllTurnsReasoningContext: (modelId: string) => boolean;
|
|
63
|
+
/**
|
|
64
|
+
* OpenAI Codex models that accept `reasoning.summary`. Shares the gpt-5.4 wire
|
|
65
|
+
* floor with {@link supportsAllTurnsReasoningContext}: earlier Codex ids
|
|
66
|
+
* (`gpt-5.1-codex`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`) reject the field
|
|
67
|
+
* with `Unsupported parameter: 'reasoning.summary' is not supported with this
|
|
68
|
+
* model`. Callers omit `summary` for unsupported ids, letting the server skip
|
|
69
|
+
* the human-readable summary stream.
|
|
70
|
+
*/
|
|
71
|
+
export declare const supportsCodexReasoningSummary: (modelId: string) => boolean;
|
|
63
72
|
/**
|
|
64
73
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
65
74
|
* `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
|
|
@@ -373,7 +373,7 @@ export declare const CATALOG_PROVIDERS: readonly [{
|
|
|
373
373
|
};
|
|
374
374
|
}, {
|
|
375
375
|
readonly id: "xiaomi";
|
|
376
|
-
readonly defaultModel: "mimo-v2
|
|
376
|
+
readonly defaultModel: "mimo-v2.5";
|
|
377
377
|
readonly envVars: readonly ["XIAOMI_API_KEY"];
|
|
378
378
|
readonly createModelManagerOptions: (config: ModelManagerConfig) => import("..").ModelManagerOptions<"openai-completions", unknown>;
|
|
379
379
|
readonly catalogDiscovery: {
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Effort } from "./effort";
|
|
2
|
+
export type { FetchImpl } from "@oh-my-pi/pi-utils";
|
|
2
3
|
export type { KnownProvider } from "./provider-models/descriptors";
|
|
3
4
|
export type KnownApi = "openai-completions" | "openai-responses" | "openrouter" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "bedrock-converse-stream" | "google-generative-ai" | "google-gemini-cli" | "google-vertex" | "ollama-chat" | "cursor-agent" | "gitlab-duo-agent" | "devin-agent";
|
|
4
5
|
export type Api = KnownApi | (string & {});
|
|
@@ -63,14 +64,6 @@ export type Provider = string;
|
|
|
63
64
|
export type ThinkingBudgets = {
|
|
64
65
|
[key in Effort]?: number;
|
|
65
66
|
};
|
|
66
|
-
/**
|
|
67
|
-
* `fetch`-compatible function. Accepts any callable matching the standard
|
|
68
|
-
* fetch signature; `preconnect` is optional because non-Bun runtimes (browsers,
|
|
69
|
-
* test mocks) won't expose it.
|
|
70
|
-
*/
|
|
71
|
-
export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
|
|
72
|
-
preconnect?: typeof globalThis.fetch.preconnect;
|
|
73
|
-
};
|
|
74
67
|
export interface Usage {
|
|
75
68
|
/** Non-cached input tokens (matches the bucket the provider bills as new input). */
|
|
76
69
|
input: number;
|
|
@@ -118,7 +111,7 @@ export interface Usage {
|
|
|
118
111
|
};
|
|
119
112
|
}
|
|
120
113
|
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
|
|
121
|
-
export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false"
|
|
114
|
+
export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
|
|
122
115
|
export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
|
|
123
116
|
/**
|
|
124
117
|
* Compatibility settings for openai-completions API.
|
|
@@ -294,13 +287,13 @@ export interface OpenAICompat {
|
|
|
294
287
|
/** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
|
|
295
288
|
supportsImageDetailOriginal?: boolean;
|
|
296
289
|
/**
|
|
297
|
-
* Append a trailing
|
|
298
|
-
*
|
|
299
|
-
*
|
|
290
|
+
* Append a trailing no-reasoning developer item when the caller did not
|
|
291
|
+
* request reasoning, suppressing default reasoning on models that cannot
|
|
292
|
+
* disable it via request params (Responses APIs only; see
|
|
300
293
|
* https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
|
|
301
|
-
* Default: auto-detected (GPT-5-family model names).
|
|
294
|
+
* The prompt must not look like an execution or tool budget. Default: auto-detected (GPT-5-family model names).
|
|
302
295
|
*/
|
|
303
|
-
|
|
296
|
+
requiresReasoningSuppressionPrompt?: boolean;
|
|
304
297
|
/** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
|
|
305
298
|
reasoningDeltasMayBeCumulative?: boolean;
|
|
306
299
|
/** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
|
|
@@ -470,7 +463,7 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
470
463
|
* `buildModel`; request handlers read fields and never detect, resolve, or
|
|
471
464
|
* allocate.
|
|
472
465
|
*/
|
|
473
|
-
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "
|
|
466
|
+
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresReasoningSuppressionPrompt" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
474
467
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
475
468
|
extraBody?: OpenAICompat["extraBody"];
|
|
476
469
|
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
@@ -489,7 +482,7 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
489
482
|
supportsLongPromptCacheRetention: boolean;
|
|
490
483
|
strictResponsesPairing: boolean;
|
|
491
484
|
supportsImageDetailOriginal: boolean;
|
|
492
|
-
|
|
485
|
+
requiresReasoningSuppressionPrompt: boolean;
|
|
493
486
|
supportsObfuscationOptOut: boolean;
|
|
494
487
|
streamIdleTimeoutMs?: number;
|
|
495
488
|
}
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { type FetchImpl } from "@oh-my-pi/pi-utils";
|
|
1
2
|
export { isRecord } from "@oh-my-pi/pi-utils";
|
|
3
|
+
/**
|
|
4
|
+
* Fetch implementation for catalog discovery probes: the caller's override
|
|
5
|
+
* when given, otherwise global fetch wrapped for `NODE_EXTRA_CA_CERTS`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function discoveryFetch(override?: FetchImpl): FetchImpl;
|
|
2
8
|
export declare function toNumber(value: unknown): number | undefined;
|
|
3
9
|
export declare function toPositiveNumber(value: unknown, fallback: number): number;
|
|
4
10
|
export declare function toPositiveNumber(value: unknown, fallback: number | null): number | null;
|
|
@@ -9,3 +9,4 @@ export interface CoreWeaveProjectEnv {
|
|
|
9
9
|
export declare function resolveCoreWeaveProject(env: CoreWeaveProjectEnv): string | undefined;
|
|
10
10
|
export declare function coreWeaveProjectHeaders(env: CoreWeaveProjectEnv): Record<string, string> | undefined;
|
|
11
11
|
export declare function hasCoreWeaveProjectHeader(headers: Record<string, string>): boolean;
|
|
12
|
+
export declare function removeBlankCoreWeaveProjectHeaders(headers: Record<string, string>): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-catalog",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.3.0",
|
|
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.0",
|
|
37
|
-
"@oh-my-pi/pi-utils": "16.
|
|
37
|
+
"@oh-my-pi/pi-utils": "16.3.0",
|
|
38
38
|
"arktype": "^2.2.0",
|
|
39
39
|
"zod": "^4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "16.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.3.0",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/compat/anthropic.ts
CHANGED
|
@@ -42,12 +42,16 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
|
|
42
42
|
const official = isOfficialAnthropicApiUrl(baseUrl);
|
|
43
43
|
// Z.AI's Anthropic-compatible proxy lives at `api.z.ai/api/anthropic`.
|
|
44
44
|
const isZai = modelMatchesHost(spec, "zai");
|
|
45
|
-
// GitHub Copilot's
|
|
45
|
+
// GitHub Copilot's `anthropic-messages` proxy (api.githubcopilot.com/v1/messages)
|
|
46
46
|
// rejects the per-tool `eager_input_streaming` field with
|
|
47
47
|
// `tools.0.custom.eager_input_streaming: Extra inputs are not permitted` and
|
|
48
48
|
// doesn't whitelist the `fine-grained-tool-streaming-2025-05-14` beta either
|
|
49
49
|
// (issue #2558), so eager tool-input streaming is unavailable on this host.
|
|
50
50
|
const isCopilot = modelMatchesHost(spec, "githubCopilot");
|
|
51
|
+
// ZenMux's `anthropic-messages` route (zenmux.ai/api/anthropic) forwards to
|
|
52
|
+
// signature-enforcing Anthropic — same failure class as GitHub Copilot #2851
|
|
53
|
+
// (issue #4192).
|
|
54
|
+
const isZenmux = modelMatchesHost(spec, "zenmux");
|
|
51
55
|
const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && matchesKimiK27CodeFamily(spec);
|
|
52
56
|
const compat: ResolvedAnthropicCompat = {
|
|
53
57
|
officialEndpoint: official,
|
|
@@ -77,15 +81,18 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res
|
|
|
77
81
|
// arguments (#2005). Known non-signing hosts (Z.AI, DeepSeek) are also
|
|
78
82
|
// preserved for compatibility.
|
|
79
83
|
//
|
|
80
|
-
// GitHub Copilot's `anthropic-messages` proxy
|
|
81
|
-
// signature-enforcing Anthropic and
|
|
82
|
-
//
|
|
83
|
-
// `signature: ""` there 400s the whole
|
|
84
|
-
// visibly when a checkpoint/branch-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// GitHub Copilot's `anthropic-messages` proxy and ZenMux's Anthropic route
|
|
85
|
+
// are excluded: both forward to signature-enforcing Anthropic and return
|
|
86
|
+
// full thinking signatures, so they are SIGNING endpoints. Replaying a
|
|
87
|
+
// stripped/unsigned thinking block as `signature: ""` there 400s the whole
|
|
88
|
+
// request ("Invalid signature") — most visibly when a checkpoint/branch-
|
|
89
|
+
// return turn's end_turn-bound signature is stripped on replay (issues
|
|
90
|
+
// #2851, #4192). Treating them like official Anthropic degrades such
|
|
91
|
+
// blocks to text instead, which the API accepts.
|
|
87
92
|
replayUnsignedThinking:
|
|
88
|
-
!isCopilot &&
|
|
93
|
+
!isCopilot &&
|
|
94
|
+
!isZenmux &&
|
|
95
|
+
(isZai || modelMatchesHost(spec, "deepseekFamily") || (spec.reasoning && !official)),
|
|
89
96
|
escapeBuiltinToolNames: modelMatchesHost(spec, "umans"),
|
|
90
97
|
};
|
|
91
98
|
applyCompatOverrides(compat, spec.compat);
|
package/src/compat/openai.ts
CHANGED
|
@@ -117,20 +117,39 @@ function resolveReasoningDisableMode(
|
|
|
117
117
|
/**
|
|
118
118
|
* Pick the leaked-markup healer for an OpenAI-compatible visible-text stream.
|
|
119
119
|
* Kimi chat-template tokens and DeepSeek DSML envelopes need their dedicated
|
|
120
|
-
* tool-call grammars
|
|
121
|
-
*
|
|
122
|
-
*
|
|
120
|
+
* tool-call grammars. Every other OpenAI-compatible model defaults to
|
|
121
|
+
* `"thinking"` so leaked reasoning idioms (e.g. a Gemini ` ```thinking ` fence
|
|
122
|
+
* on OpenRouter) are recovered from `delta.content` — **except** the official
|
|
123
|
+
* OpenAI endpoint (`provider: "openai"` + `api.openai.com`), which returns
|
|
124
|
+
* structured reasoning and never leaks, so it heals nothing (returns
|
|
125
|
+
* `undefined`) to avoid misfiring on legitimate fenced content.
|
|
123
126
|
*/
|
|
124
|
-
function detectStreamMarkupHealingPattern(
|
|
127
|
+
function detectStreamMarkupHealingPattern(
|
|
128
|
+
provider: string,
|
|
129
|
+
modelId: string,
|
|
130
|
+
baseUrl: string,
|
|
131
|
+
): OpenAIStreamMarkupHealingPattern | undefined {
|
|
125
132
|
if (provider === "kimi-code" || provider === "moonshot" || /kimi[-/_.]?k2/i.test(modelId)) {
|
|
126
133
|
return "kimi";
|
|
127
134
|
}
|
|
128
135
|
if (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {
|
|
129
136
|
return "dsml";
|
|
130
137
|
}
|
|
138
|
+
if (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined;
|
|
131
139
|
return "thinking";
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
/** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host (missing baseUrl defaults there). */
|
|
143
|
+
function isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean {
|
|
144
|
+
if (provider !== "openai") return false;
|
|
145
|
+
if (!baseUrl) return true;
|
|
146
|
+
try {
|
|
147
|
+
return new URL(baseUrl).hostname === "api.openai.com";
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
/**
|
|
135
154
|
* OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`
|
|
136
155
|
* on the request's thinking state for every model they front (Kimi K2.x,
|
|
@@ -517,7 +536,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
517
536
|
streamIdleTimeoutMs,
|
|
518
537
|
stripDeepseekSpecialTokens:
|
|
519
538
|
isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
|
|
520
|
-
streamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id),
|
|
539
|
+
streamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id, baseUrl),
|
|
521
540
|
reasoningDeltasMayBeCumulative:
|
|
522
541
|
MINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),
|
|
523
542
|
emptyLengthFinishIsContextError: provider === "ollama",
|
|
@@ -574,8 +593,8 @@ interface OpenAIResponsesSpecLike {
|
|
|
574
593
|
* as well as URL — bundled `azure` models carry no baseUrl (the deployment host
|
|
575
594
|
* is per-resource, resolved at runtime) — while OpenAI/Copilot developer-role
|
|
576
595
|
* and prompt-cache detection stay URL-keyed, as the historical call sites were.
|
|
577
|
-
* The GPT-5
|
|
578
|
-
* request-time check.
|
|
596
|
+
* The GPT-5 reasoning-suppression prompt keys on the model name, matching the
|
|
597
|
+
* historical request-time check.
|
|
579
598
|
*/
|
|
580
599
|
export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
|
|
581
600
|
const baseUrl = spec.baseUrl ?? "";
|
|
@@ -607,7 +626,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
607
626
|
// or base-URL host (mirroring the Anthropic compat builder) so a model
|
|
608
627
|
// pointed at the Copilot host under a different provider id still clamps.
|
|
609
628
|
supportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
|
|
610
|
-
|
|
629
|
+
requiresReasoningSuppressionPrompt: spec.name.toLowerCase().startsWith("gpt-5"),
|
|
611
630
|
reasoningEffortMap: {},
|
|
612
631
|
supportsReasoningParams: true,
|
|
613
632
|
thinkingFormat,
|
|
@@ -646,7 +665,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
646
665
|
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
|
647
666
|
stripDeepseekSpecialTokens:
|
|
648
667
|
Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
|
|
649
|
-
streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id) : undefined,
|
|
668
|
+
streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,
|
|
650
669
|
reasoningDeltasMayBeCumulative:
|
|
651
670
|
MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
|
|
652
671
|
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
|
@@ -674,7 +693,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
|
|
|
674
693
|
supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
|
|
675
694
|
strictResponsesPairing: compat.strictResponsesPairing,
|
|
676
695
|
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
|
|
677
|
-
|
|
696
|
+
requiresReasoningSuppressionPrompt: compat.requiresReasoningSuppressionPrompt,
|
|
678
697
|
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
|
679
698
|
} satisfies ResponsesOnlyCompat;
|
|
680
699
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
2
|
import type { ModelSpec } from "../types";
|
|
3
|
-
import { toPositiveNumber } from "../utils";
|
|
3
|
+
import { discoveryFetch, toPositiveNumber } from "../utils";
|
|
4
4
|
import {
|
|
5
5
|
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
|
|
6
6
|
collapseEffortVariants,
|
|
@@ -161,7 +161,7 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
161
161
|
export async function fetchAntigravityDiscoveryModels(
|
|
162
162
|
options: FetchAntigravityDiscoveryModelsOptions,
|
|
163
163
|
): Promise<ModelSpec<"google-gemini-cli">[] | null> {
|
|
164
|
-
const fetcher = options.fetcher
|
|
164
|
+
const fetcher = discoveryFetch(options.fetcher);
|
|
165
165
|
const endpoints = options.endpoint
|
|
166
166
|
? [trimTrailingSlashes(options.endpoint)]
|
|
167
167
|
: DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
|
package/src/discovery/codex.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { FetchImpl } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import { type } from "arktype";
|
|
2
3
|
import type { ModelSpec } from "../types";
|
|
3
|
-
import { isRecord } from "../utils";
|
|
4
|
+
import { discoveryFetch, isRecord } from "../utils";
|
|
4
5
|
import { CODEX_BASE_URL, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../wire/codex";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_MODEL_LIST_PATHS = ["/codex/models", "/models"] as const;
|
|
@@ -8,6 +9,11 @@ const DEFAULT_CONTEXT_WINDOW = 272_000;
|
|
|
8
9
|
const DEFAULT_MAX_TOKENS = 128_000;
|
|
9
10
|
const DEFAULT_CODEX_CLIENT_VERSION = "0.99.0";
|
|
10
11
|
const NPM_CODEX_LATEST_URL = "https://registry.npmjs.org/@openai%2Fcodex/latest";
|
|
12
|
+
const CODEX_REMOTE_COMPACTION = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
api: "openai-codex-responses",
|
|
15
|
+
v2StreamingEnabled: true,
|
|
16
|
+
} as const;
|
|
11
17
|
|
|
12
18
|
const codexReasoningPresetSchema = type({
|
|
13
19
|
"effort?": "unknown",
|
|
@@ -76,7 +82,7 @@ export interface CodexModelDiscoveryResult {
|
|
|
76
82
|
* Returns `{ models: [] }` when a route succeeds but yields no usable models.
|
|
77
83
|
*/
|
|
78
84
|
export async function fetchCodexModels(options: CodexModelDiscoveryOptions): Promise<CodexModelDiscoveryResult | null> {
|
|
79
|
-
const fetchFn = options.fetchFn
|
|
85
|
+
const fetchFn = discoveryFetch(options.fetchFn);
|
|
80
86
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
81
87
|
const paths = normalizePaths(options.paths);
|
|
82
88
|
const headers = buildCodexHeaders(options);
|
|
@@ -163,7 +169,7 @@ function buildCodexHeaders(options: CodexModelDiscoveryOptions): Headers {
|
|
|
163
169
|
|
|
164
170
|
async function resolveCodexClientVersion(
|
|
165
171
|
clientVersion: string | undefined,
|
|
166
|
-
fetchFn:
|
|
172
|
+
fetchFn: FetchImpl,
|
|
167
173
|
signal: AbortSignal | undefined,
|
|
168
174
|
): Promise<string> {
|
|
169
175
|
const normalizedClientVersion = normalizeClientVersion(clientVersion);
|
|
@@ -269,6 +275,7 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
|
|
|
269
275
|
reasoning,
|
|
270
276
|
input,
|
|
271
277
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
278
|
+
remoteCompaction: CODEX_REMOTE_COMPACTION,
|
|
272
279
|
contextWindow,
|
|
273
280
|
maxTokens,
|
|
274
281
|
...(preferWebsockets ? { preferWebsockets: true } : {}),
|
package/src/discovery/devin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { gunzipSync } from "node:zlib";
|
|
2
2
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
3
|
import type { FetchImpl, ModelSpec } from "../types";
|
|
4
|
+
import { discoveryFetch } from "../utils";
|
|
4
5
|
import {
|
|
5
6
|
GetCliModelConfigsRequestSchema,
|
|
6
7
|
GetCliModelConfigsResponseSchema,
|
|
@@ -76,7 +77,7 @@ export async function fetchDevinModels(
|
|
|
76
77
|
accept: "*/*",
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
const fetchImpl = options.fetch
|
|
80
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
80
81
|
const response = await fetchImpl(requestUrl, { method: "POST", headers, body, signal });
|
|
81
82
|
if (!response.ok) {
|
|
82
83
|
return null;
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type } from "arktype";
|
|
|
2
2
|
import { getBundledModels } from "../models";
|
|
3
3
|
import { toModelSpec } from "../provider-models/bundled-references";
|
|
4
4
|
import type { FetchImpl, Model, ModelSpec } from "../types";
|
|
5
|
+
import { discoveryFetch } from "../utils";
|
|
5
6
|
|
|
6
7
|
const GOOGLE_GENERATIVE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
7
8
|
const DEFAULT_PAGE_SIZE = 100;
|
|
@@ -77,7 +78,7 @@ export async function fetchGeminiModels(
|
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const fetchImpl = options.fetch
|
|
81
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
81
82
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
82
83
|
const pageSize = normalizePositiveInt(options.pageSize, DEFAULT_PAGE_SIZE);
|
|
83
84
|
const maxPages = normalizePositiveInt(options.maxPages, DEFAULT_MAX_PAGES);
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
4
|
import type { FetchImpl, ModelSpec } from "../types";
|
|
5
|
-
import { isRecord } from "../utils";
|
|
5
|
+
import { discoveryFetch, isRecord } from "../utils";
|
|
6
6
|
|
|
7
7
|
const GITLAB_DEFAULT_BASE_URL = "https://gitlab.com";
|
|
8
8
|
const GRAPHQL_PATH = "/api/graphql";
|
|
@@ -340,7 +340,7 @@ async function fetchNamespaceOverrideCandidate(
|
|
|
340
340
|
if (!restNamespaceId) {
|
|
341
341
|
return null;
|
|
342
342
|
}
|
|
343
|
-
const fetchImpl = config.fetch
|
|
343
|
+
const fetchImpl = discoveryFetch(config.fetch);
|
|
344
344
|
let response: Response;
|
|
345
345
|
try {
|
|
346
346
|
response = await fetchImpl(`${baseUrl}${GROUPS_PATH}/${encodeURIComponent(restNamespaceId)}`, {
|
|
@@ -406,7 +406,7 @@ async function fetchProjectRootNamespaceViaRest(
|
|
|
406
406
|
baseUrl: string,
|
|
407
407
|
projectIdOrPath: string,
|
|
408
408
|
): Promise<GitLabDuoWorkflowRestProject | null> {
|
|
409
|
-
const fetchImpl = config.fetch
|
|
409
|
+
const fetchImpl = discoveryFetch(config.fetch);
|
|
410
410
|
let response: Response;
|
|
411
411
|
try {
|
|
412
412
|
response = await fetchImpl(`${baseUrl}${PROJECTS_PATH}/${encodeURIComponent(projectIdOrPath)}`, {
|
|
@@ -449,7 +449,7 @@ async function fetchTopLevelGroupNamespaceCandidates(
|
|
|
449
449
|
config: GitLabDuoWorkflowDiscoveryConfig,
|
|
450
450
|
baseUrl: string,
|
|
451
451
|
): Promise<GitLabDuoWorkflowCandidate[]> {
|
|
452
|
-
const fetchImpl = config.fetch
|
|
452
|
+
const fetchImpl = discoveryFetch(config.fetch);
|
|
453
453
|
const candidates: (GitLabDuoWorkflowCandidate & { preferred: boolean })[] = [];
|
|
454
454
|
// GitLab paginates `/groups`; a token can belong to more than one page of top-level
|
|
455
455
|
// groups, and a usable Duo namespace may live on a later page. Follow the keyset/
|
|
@@ -518,7 +518,7 @@ async function postGraphQL(
|
|
|
518
518
|
query: string,
|
|
519
519
|
variables: Record<string, string>,
|
|
520
520
|
): Promise<unknown | null> {
|
|
521
|
-
const fetchImpl = config.fetch
|
|
521
|
+
const fetchImpl = discoveryFetch(config.fetch);
|
|
522
522
|
let response: Response;
|
|
523
523
|
try {
|
|
524
524
|
response = await fetchImpl(`${baseUrl}${GRAPHQL_PATH}`, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
2
|
import type { Api, FetchImpl, ModelSpec, Provider } from "../types";
|
|
3
|
+
import { discoveryFetch } from "../utils";
|
|
3
4
|
|
|
4
5
|
const MODELS_PATH = "/models";
|
|
5
6
|
|
|
@@ -137,7 +138,7 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
|
|
|
137
138
|
requestHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
const fetchImpl = options.fetch
|
|
141
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
141
142
|
const fetchPayload = async (signal?: AbortSignal): Promise<unknown | null> => {
|
|
142
143
|
let response: Response;
|
|
143
144
|
try {
|
package/src/hosts.ts
CHANGED
|
@@ -59,6 +59,8 @@ export const KNOWN_HOSTS = {
|
|
|
59
59
|
nvidia: { providers: ["nvidia"], urlMarkers: ["integrate.api.nvidia.com"] },
|
|
60
60
|
moonshotNative: { providers: ["moonshot", "kimi-code"], urlMarkers: ["api.moonshot.ai", "api.kimi.com"] },
|
|
61
61
|
opencode: { providers: ["opencode-go", "opencode-zen"], urlMarkers: ["opencode.ai"] },
|
|
62
|
+
/** ZenMux's Anthropic-compatible proxy (`zenmux.ai/api/anthropic`) forwards to signature-enforcing Anthropic. */
|
|
63
|
+
zenmux: { providers: ["zenmux"], urlMarkers: ["zenmux.ai"] },
|
|
62
64
|
chutes: { urlMarkers: ["chutes.ai"] },
|
|
63
65
|
} as const satisfies Record<string, HostClassSpec>;
|
|
64
66
|
|
package/src/identity/family.ts
CHANGED
|
@@ -123,6 +123,13 @@ export const isOpenAIModelId = memo((modelId: string): boolean => {
|
|
|
123
123
|
return /(^|\/)(gpt|o1|o3|o4)[-.]/i.test(modelId) || modelId.toLowerCase().includes("openai/");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
/** OpenAI models at or above the gpt-5.4 wire generation, keyed off the parsed version. */
|
|
127
|
+
const isOpenAIWireGen54Plus = memo((modelId: string): boolean => {
|
|
128
|
+
const parsed = parseOpenAIModel(bareModelId(modelId));
|
|
129
|
+
if (!parsed) return false;
|
|
130
|
+
return semverGte(parsed.version, "5.4");
|
|
131
|
+
});
|
|
132
|
+
|
|
126
133
|
/**
|
|
127
134
|
* OpenAI Codex models that honor `reasoning.context: "all_turns"` (full
|
|
128
135
|
* cross-turn reasoning replay). The `reasoning.context` field itself exists for
|
|
@@ -133,11 +140,17 @@ export const isOpenAIModelId = memo((modelId: string): boolean => {
|
|
|
133
140
|
* floor (not an allowlist) so 5.6/6.x inherit support automatically. Callers
|
|
134
141
|
* fall back to omitting `context`, letting the server default to `current_turn`.
|
|
135
142
|
*/
|
|
136
|
-
export const supportsAllTurnsReasoningContext =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
143
|
+
export const supportsAllTurnsReasoningContext = isOpenAIWireGen54Plus;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* OpenAI Codex models that accept `reasoning.summary`. Shares the gpt-5.4 wire
|
|
147
|
+
* floor with {@link supportsAllTurnsReasoningContext}: earlier Codex ids
|
|
148
|
+
* (`gpt-5.1-codex`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`) reject the field
|
|
149
|
+
* with `Unsupported parameter: 'reasoning.summary' is not supported with this
|
|
150
|
+
* model`. Callers omit `summary` for unsupported ids, letting the server skip
|
|
151
|
+
* the human-readable summary stream.
|
|
152
|
+
*/
|
|
153
|
+
export const supportsCodexReasoningSummary = isOpenAIWireGen54Plus;
|
|
141
154
|
|
|
142
155
|
/**
|
|
143
156
|
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
package/src/model-cache.ts
CHANGED
|
@@ -7,14 +7,15 @@ 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
|
-
//
|
|
17
|
-
|
|
10
|
+
// the model manager rebuilds via `buildModel` on load. v8 invalidates Codex
|
|
11
|
+
// discovery rows predating provider-native V2 compaction metadata; v7
|
|
12
|
+
// invalidated rows predating the Antigravity Gemini budget-mode migration
|
|
13
|
+
// (cached specs still carrying `thinking.mode: "google-level"` and the old
|
|
14
|
+
// 3.5-flash effort routing); v6 invalidated rows that may contain the retired
|
|
15
|
+
// unknown-limit sentinels (222222/8888); v5 invalidated rows predating
|
|
16
|
+
// effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
|
|
17
|
+
// v4 dropped the pre-efforts ThinkingConfig shape.
|
|
18
|
+
const CACHE_SCHEMA_VERSION = 8;
|
|
18
19
|
|
|
19
20
|
interface CacheRow {
|
|
20
21
|
provider_id: string;
|
|
@@ -100,7 +101,12 @@ function migrateCacheSchema(db: Database): void {
|
|
|
100
101
|
} finally {
|
|
101
102
|
stmt.finalize();
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
// Delete rows written under any older schema so they cannot be reused. The
|
|
105
|
+
// legacy `UPDATE ... WHERE version = 2` migration silently promoted the very
|
|
106
|
+
// first cache version to whatever the current one is, defeating every
|
|
107
|
+
// subsequent invalidation (see #4146: pre-V2 Codex rows kept the legacy
|
|
108
|
+
// compaction path even after CACHE_SCHEMA_VERSION was bumped).
|
|
109
|
+
db.run("DELETE FROM model_cache WHERE version <> ?", [CACHE_SCHEMA_VERSION]);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export function readModelCache<TApi extends Api>(
|
|
@@ -415,7 +415,7 @@ export const CATALOG_PROVIDERS = [
|
|
|
415
415
|
},
|
|
416
416
|
{
|
|
417
417
|
id: "xiaomi",
|
|
418
|
-
defaultModel: "mimo-v2
|
|
418
|
+
defaultModel: "mimo-v2.5",
|
|
419
419
|
envVars: ["XIAOMI_API_KEY"],
|
|
420
420
|
createModelManagerOptions: (config: ModelManagerConfig) => xiaomiModelManagerOptions(config),
|
|
421
421
|
catalogDiscovery: { label: "Xiaomi" },
|
|
@@ -3,6 +3,7 @@ import { Effort } from "../effort";
|
|
|
3
3
|
import { isGlm52ReasoningEffortModelId } from "../identity/family";
|
|
4
4
|
import type { ModelManagerOptions } from "../model-manager";
|
|
5
5
|
import type { FetchImpl, ThinkingConfig } from "../types";
|
|
6
|
+
import { discoveryFetch } from "../utils";
|
|
6
7
|
import { createBundledReferenceMap, createReferenceResolver } from "./bundled-references";
|
|
7
8
|
|
|
8
9
|
export interface OllamaCloudModelManagerConfig {
|
|
@@ -75,7 +76,7 @@ async function fetchShowMetadata(
|
|
|
75
76
|
baseUrl: string,
|
|
76
77
|
apiKey: string,
|
|
77
78
|
model: string,
|
|
78
|
-
fetchImpl: FetchImpl =
|
|
79
|
+
fetchImpl: FetchImpl = discoveryFetch(),
|
|
79
80
|
): Promise<OllamaShowResponse | undefined> {
|
|
80
81
|
const response = await fetchImpl(`${baseUrl}/api/show`, {
|
|
81
82
|
method: "POST",
|
|
@@ -107,7 +108,7 @@ export function ollamaCloudModelManagerOptions(
|
|
|
107
108
|
const response = await fetchWithRetry(`${baseUrl}/api/tags`, {
|
|
108
109
|
method: "GET",
|
|
109
110
|
headers: createCloudHeaders(apiKey),
|
|
110
|
-
fetch: config?.fetch,
|
|
111
|
+
fetch: discoveryFetch(config?.fetch),
|
|
111
112
|
defaultDelayMs: OLLAMA_RETRY_DELAYS_MS,
|
|
112
113
|
});
|
|
113
114
|
if (!response.ok) {
|
|
@@ -9,7 +9,7 @@ import { isGlmVisionModelId, isGrokReasoningEffortCapable, isReasoningGlmModelId
|
|
|
9
9
|
import type { ModelManagerOptions } from "../model-manager";
|
|
10
10
|
import { getBundledModels } from "../models";
|
|
11
11
|
import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from "../types";
|
|
12
|
-
import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
12
|
+
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
13
13
|
import { coreWeaveProjectHeaders } from "../wire/coreweave";
|
|
14
14
|
import {
|
|
15
15
|
COPILOT_API_HEADERS,
|
|
@@ -81,7 +81,7 @@ function toInputCapabilities(value: unknown): ("text" | "image")[] {
|
|
|
81
81
|
return supportsImage ? ["text", "image"] : ["text"];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async function fetchModelsDevPayload(fetchImpl: FetchImpl =
|
|
84
|
+
async function fetchModelsDevPayload(fetchImpl: FetchImpl = discoveryFetch()): Promise<unknown> {
|
|
85
85
|
const response = await fetchImpl(MODELS_DEV_URL, {
|
|
86
86
|
method: "GET",
|
|
87
87
|
headers: { Accept: "application/json" },
|
|
@@ -270,7 +270,7 @@ function toOllamaNativeBaseUrl(baseUrl: string): string {
|
|
|
270
270
|
async function fetchOllamaNativeModels(
|
|
271
271
|
baseUrl: string,
|
|
272
272
|
resolveMetadata: (modelId: string) => Promise<OllamaResolvedMetadata>,
|
|
273
|
-
fetchImpl: FetchImpl =
|
|
273
|
+
fetchImpl: FetchImpl = discoveryFetch(),
|
|
274
274
|
): Promise<ModelSpec<"openai-responses">[] | null> {
|
|
275
275
|
const nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);
|
|
276
276
|
let response: Response;
|
|
@@ -376,7 +376,7 @@ function getOllamaThinkingConfig(capabilities: string[] | undefined): ThinkingCo
|
|
|
376
376
|
async function fetchOllamaShowMetadata(
|
|
377
377
|
nativeBaseUrl: string,
|
|
378
378
|
modelId: string,
|
|
379
|
-
fetchImpl: FetchImpl =
|
|
379
|
+
fetchImpl: FetchImpl = discoveryFetch(),
|
|
380
380
|
): Promise<OllamaShowMetadata | undefined> {
|
|
381
381
|
try {
|
|
382
382
|
const response = await fetchImpl(`${nativeBaseUrl}/api/show`, {
|
|
@@ -743,7 +743,7 @@ async function fetchUmansModelsInfo(options: {
|
|
|
743
743
|
if (options.apiKey) {
|
|
744
744
|
requestHeaders["x-api-key"] = options.apiKey;
|
|
745
745
|
}
|
|
746
|
-
const fetchImpl = options.fetch
|
|
746
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
747
747
|
let payload: unknown;
|
|
748
748
|
try {
|
|
749
749
|
const response = await fetchImpl(`${discoveryBaseUrl}${UMANS_MODELS_INFO_PATH}`, {
|
|
@@ -1541,7 +1541,7 @@ async function fetchFireworksServerlessModels(options: {
|
|
|
1541
1541
|
}): Promise<ModelSpec<"openai-completions">[] | null> {
|
|
1542
1542
|
const listUrl = toFireworksControlPlaneModelsUrl(options.baseUrl, FIREWORKS_CONTROL_PLANE_ACCOUNT);
|
|
1543
1543
|
if (!listUrl) return null;
|
|
1544
|
-
const fetchImpl = options.fetch
|
|
1544
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
1545
1545
|
const collected = new Map<string, ModelSpec<"openai-completions">>();
|
|
1546
1546
|
let pageToken = "";
|
|
1547
1547
|
for (let page = 0; page < FIREWORKS_CONTROL_PLANE_MAX_PAGES; page++) {
|
|
@@ -2926,6 +2926,33 @@ function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
|
|
|
2926
2926
|
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2927
2927
|
}
|
|
2928
2928
|
|
|
2929
|
+
const LITELLM_RESELLER_USAGE_SUFFIX = /\s+\(\d+(?:\.\d+)?[x×] usage\)$/i;
|
|
2930
|
+
|
|
2931
|
+
function stripLiteLLMResellerUsageSuffix(name: string): string {
|
|
2932
|
+
const cleaned = name.replace(LITELLM_RESELLER_USAGE_SUFFIX, "").trim();
|
|
2933
|
+
return cleaned.length > 0 ? cleaned : name;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
function toLiteLLMDisplayName(modelName: string | undefined, referenceName: string | undefined, id: string): string {
|
|
2937
|
+
const cleanedModelName = modelName ? stripLiteLLMResellerUsageSuffix(modelName) : undefined;
|
|
2938
|
+
if (cleanedModelName && cleanedModelName !== id) {
|
|
2939
|
+
return cleanedModelName;
|
|
2940
|
+
}
|
|
2941
|
+
return referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function mapLiteLLMOpenAICompatibleModel<TApi extends Api>(
|
|
2945
|
+
entry: OpenAICompatibleModelRecord,
|
|
2946
|
+
defaults: ModelSpec<TApi>,
|
|
2947
|
+
reference: ModelSpec<TApi> | undefined,
|
|
2948
|
+
): ModelSpec<TApi> {
|
|
2949
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2950
|
+
return {
|
|
2951
|
+
...model,
|
|
2952
|
+
name: stripLiteLLMResellerUsageSuffix(model.name),
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2929
2956
|
function toNonEmptyString(value: unknown): string | undefined {
|
|
2930
2957
|
if (typeof value !== "string") {
|
|
2931
2958
|
return undefined;
|
|
@@ -3025,7 +3052,7 @@ function mapLiteLLMRichEntry<TApi extends Api>(
|
|
|
3025
3052
|
};
|
|
3026
3053
|
return {
|
|
3027
3054
|
id,
|
|
3028
|
-
name: modelName
|
|
3055
|
+
name: toLiteLLMDisplayName(modelName, reference?.name, id),
|
|
3029
3056
|
api: options.api,
|
|
3030
3057
|
provider: options.provider,
|
|
3031
3058
|
baseUrl: runtimeBaseUrl,
|
|
@@ -3052,7 +3079,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
3052
3079
|
runtimeBaseUrl: string,
|
|
3053
3080
|
signal?: AbortSignal,
|
|
3054
3081
|
): Promise<ModelSpec<TApi>[] | null> {
|
|
3055
|
-
const fetchImpl = options.fetch
|
|
3082
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
3056
3083
|
const requestHeaders: Record<string, string> = {
|
|
3057
3084
|
Accept: "application/json",
|
|
3058
3085
|
...options.headers,
|
|
@@ -3126,7 +3153,11 @@ export function litellmModelManagerOptions(
|
|
|
3126
3153
|
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
3127
3154
|
return {
|
|
3128
3155
|
providerId: "litellm",
|
|
3129
|
-
|
|
3156
|
+
// rich-v2 invalidates rows cached before reseller usage-suffix stripping
|
|
3157
|
+
// (stale display names like `MiniMax-M3 (3x usage)`); bump the version
|
|
3158
|
+
// whenever the mappers below change, or warm authoritative caches keep
|
|
3159
|
+
// serving pre-change rows for the full TTL.
|
|
3160
|
+
cacheProviderId: `litellm:rich-v2:${Bun.hash(baseUrl).toString(36)}`,
|
|
3130
3161
|
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3131
3162
|
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3132
3163
|
// management metadata, then fall back to /v1/models and enrich bare ids
|
|
@@ -3151,7 +3182,8 @@ export function litellmModelManagerOptions(
|
|
|
3151
3182
|
provider: "litellm",
|
|
3152
3183
|
baseUrl,
|
|
3153
3184
|
apiKey,
|
|
3154
|
-
mapModel: (entry, defaults) =>
|
|
3185
|
+
mapModel: (entry, defaults) =>
|
|
3186
|
+
mapLiteLLMOpenAICompatibleModel(entry, defaults, resolveReference(defaults.id)),
|
|
3155
3187
|
fetch: config?.fetch,
|
|
3156
3188
|
});
|
|
3157
3189
|
},
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Effort } from "./effort";
|
|
2
2
|
|
|
3
|
+
// Re-exported from @oh-my-pi/pi-utils so the whole workspace shares one
|
|
4
|
+
// `fetch`-compatible signature (tls-fetch's wrappers produce/accept it).
|
|
5
|
+
export type { FetchImpl } from "@oh-my-pi/pi-utils";
|
|
3
6
|
export type { KnownProvider } from "./provider-models/descriptors";
|
|
4
7
|
|
|
5
8
|
export type KnownApi =
|
|
@@ -89,15 +92,6 @@ export type Provider = string;
|
|
|
89
92
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
90
93
|
export type ThinkingBudgets = { [key in Effort]?: number };
|
|
91
94
|
|
|
92
|
-
/**
|
|
93
|
-
* `fetch`-compatible function. Accepts any callable matching the standard
|
|
94
|
-
* fetch signature; `preconnect` is optional because non-Bun runtimes (browsers,
|
|
95
|
-
* test mocks) won't expose it.
|
|
96
|
-
*/
|
|
97
|
-
export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
|
|
98
|
-
preconnect?: typeof globalThis.fetch.preconnect;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
95
|
export interface Usage {
|
|
102
96
|
/** Non-cached input tokens (matches the bucket the provider bills as new input). */
|
|
103
97
|
input: number;
|
|
@@ -153,8 +147,7 @@ export type OpenAIReasoningDisableMode =
|
|
|
153
147
|
| "openrouter-enabled-false"
|
|
154
148
|
| "zai-thinking-disabled"
|
|
155
149
|
| "qwen-enable-thinking-false"
|
|
156
|
-
| "qwen-template-false"
|
|
157
|
-
| "juice-zero-developer-message";
|
|
150
|
+
| "qwen-template-false";
|
|
158
151
|
|
|
159
152
|
export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
|
|
160
153
|
|
|
@@ -332,13 +325,13 @@ export interface OpenAICompat {
|
|
|
332
325
|
/** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
|
|
333
326
|
supportsImageDetailOriginal?: boolean;
|
|
334
327
|
/**
|
|
335
|
-
* Append a trailing
|
|
336
|
-
*
|
|
337
|
-
*
|
|
328
|
+
* Append a trailing no-reasoning developer item when the caller did not
|
|
329
|
+
* request reasoning, suppressing default reasoning on models that cannot
|
|
330
|
+
* disable it via request params (Responses APIs only; see
|
|
338
331
|
* https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
|
|
339
|
-
* Default: auto-detected (GPT-5-family model names).
|
|
332
|
+
* The prompt must not look like an execution or tool budget. Default: auto-detected (GPT-5-family model names).
|
|
340
333
|
*/
|
|
341
|
-
|
|
334
|
+
requiresReasoningSuppressionPrompt?: boolean;
|
|
342
335
|
/** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
|
|
343
336
|
reasoningDeltasMayBeCumulative?: boolean;
|
|
344
337
|
/** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
|
|
@@ -564,7 +557,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
564
557
|
| "thinkingKeep"
|
|
565
558
|
| "strictResponsesPairing"
|
|
566
559
|
| "supportsImageDetailOriginal"
|
|
567
|
-
| "
|
|
560
|
+
| "requiresReasoningSuppressionPrompt"
|
|
568
561
|
| "enableGeminiThinkingLoopGuard"
|
|
569
562
|
| "whenThinking"
|
|
570
563
|
>
|
|
@@ -588,7 +581,7 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
588
581
|
supportsLongPromptCacheRetention: boolean;
|
|
589
582
|
strictResponsesPairing: boolean;
|
|
590
583
|
supportsImageDetailOriginal: boolean;
|
|
591
|
-
|
|
584
|
+
requiresReasoningSuppressionPrompt: boolean;
|
|
592
585
|
supportsObfuscationOptOut: boolean;
|
|
593
586
|
streamIdleTimeoutMs?: number;
|
|
594
587
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { type FetchImpl, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
export { isRecord } from "@oh-my-pi/pi-utils";
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Fetch implementation for catalog discovery probes: the caller's override
|
|
7
|
+
* when given, otherwise global fetch wrapped for `NODE_EXTRA_CA_CERTS`.
|
|
8
|
+
*/
|
|
9
|
+
export function discoveryFetch(override?: FetchImpl): FetchImpl {
|
|
10
|
+
return override ?? wrapFetchForExtraCa(fetch);
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
export function toNumber(value: unknown): number | undefined {
|
|
4
14
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
5
15
|
return value;
|
package/src/wire/coreweave.ts
CHANGED
|
@@ -38,5 +38,14 @@ export function coreWeaveProjectHeaders(env: CoreWeaveProjectEnv): Record<string
|
|
|
38
38
|
|
|
39
39
|
export function hasCoreWeaveProjectHeader(headers: Record<string, string>): boolean {
|
|
40
40
|
const normalized = COREWEAVE_PROJECT_HEADER.toLowerCase();
|
|
41
|
-
return Object.
|
|
41
|
+
return Object.entries(headers).some(([header, value]) => header.toLowerCase() === normalized && value.trim() !== "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function removeBlankCoreWeaveProjectHeaders(headers: Record<string, string>): void {
|
|
45
|
+
const normalized = COREWEAVE_PROJECT_HEADER.toLowerCase();
|
|
46
|
+
for (const [header, value] of Object.entries(headers)) {
|
|
47
|
+
if (header.toLowerCase() === normalized && value.trim() === "") {
|
|
48
|
+
delete headers[header];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
42
51
|
}
|