@oh-my-pi/pi-catalog 16.2.13 → 16.3.2
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 +29 -1
- package/dist/types/compat/openai.d.ts +0 -2
- package/dist/types/hosts.d.ts +5 -0
- package/dist/types/provider-models/descriptors.d.ts +3 -1
- package/dist/types/types.d.ts +3 -19
- 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 +25 -10
- package/src/discovery/antigravity.ts +2 -2
- package/src/discovery/codex.ts +4 -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/provider-models/descriptors.ts +3 -2
- package/src/provider-models/ollama.ts +3 -2
- package/src/provider-models/openai-compat.ts +76 -42
- package/src/types.ts +4 -21
- package/src/utils.ts +10 -0
- package/src/wire/coreweave.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.2] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed ZenMux model discovery to run without a `ZENMUX_API_KEY`, so newly published ZenMux models (for example `anthropic/claude-fable-5-free`) auto-update into the runtime `models.db` cache instead of waiting on a regenerated `models.json`.
|
|
10
|
+
- Fixed ZenMux runtime discovery to query the `/api/v1/models` endpoint even when the resolved provider base URL points at the Anthropic-compatible route, so discovery no longer requests a non-existent `/api/anthropic/models` path.
|
|
11
|
+
|
|
12
|
+
## [16.3.1] - 2026-07-02
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- Removed reasoning suppression prompt logic for GPT-5 models
|
|
17
|
+
|
|
18
|
+
## [16.3.0] - 2026-07-02
|
|
19
|
+
|
|
20
|
+
### Breaking Changes
|
|
21
|
+
|
|
22
|
+
- Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `"juice-zero-developer-message"` member from `OpenAIReasoningDisableMode`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.
|
|
27
|
+
- Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.
|
|
28
|
+
- 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.
|
|
29
|
+
- Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.
|
|
30
|
+
- Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.
|
|
31
|
+
- 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))
|
|
32
|
+
|
|
5
33
|
## [16.2.13] - 2026-07-01
|
|
6
34
|
|
|
7
35
|
### Added
|
|
@@ -184,7 +212,7 @@
|
|
|
184
212
|
|
|
185
213
|
### Fixed
|
|
186
214
|
|
|
187
|
-
- 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
|
|
215
|
+
- 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))
|
|
188
216
|
|
|
189
217
|
## [16.1.3] - 2026-06-19
|
|
190
218
|
|
|
@@ -20,8 +20,6 @@ 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 juice-zero hack keys on the model name, matching the historical
|
|
24
|
-
* request-time check.
|
|
25
23
|
*/
|
|
26
24
|
export declare function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat;
|
|
27
25
|
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
|
};
|
|
@@ -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: {
|
|
@@ -407,8 +407,10 @@ export declare const CATALOG_PROVIDERS: readonly [{
|
|
|
407
407
|
readonly defaultModel: "anthropic/claude-opus-4.8";
|
|
408
408
|
readonly envVars: readonly ["ZENMUX_API_KEY"];
|
|
409
409
|
readonly createModelManagerOptions: (config: ModelManagerConfig) => import("..").ModelManagerOptions<import("..").Api, unknown>;
|
|
410
|
+
readonly allowUnauthenticated: true;
|
|
410
411
|
readonly catalogDiscovery: {
|
|
411
412
|
readonly label: "ZenMux";
|
|
413
|
+
readonly allowUnauthenticated: true;
|
|
412
414
|
};
|
|
413
415
|
}, {
|
|
414
416
|
readonly id: "zhipu-coding-plan";
|
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.
|
|
@@ -293,14 +286,6 @@ export interface OpenAICompat {
|
|
|
293
286
|
strictResponsesPairing?: boolean;
|
|
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
|
-
/**
|
|
297
|
-
* Append a trailing `# Juice: 0 !important` developer item when the caller
|
|
298
|
-
* did not request reasoning, suppressing default reasoning on models that
|
|
299
|
-
* cannot disable it via request params (Responses APIs only; see
|
|
300
|
-
* https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
|
|
301
|
-
* Default: auto-detected (GPT-5-family model names).
|
|
302
|
-
*/
|
|
303
|
-
requiresJuiceZeroHack?: boolean;
|
|
304
289
|
/** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
|
|
305
290
|
reasoningDeltasMayBeCumulative?: boolean;
|
|
306
291
|
/** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
|
|
@@ -470,7 +455,7 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
470
455
|
* `buildModel`; request handlers read fields and never detect, resolve, or
|
|
471
456
|
* allocate.
|
|
472
457
|
*/
|
|
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" | "
|
|
458
|
+
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" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
474
459
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
475
460
|
extraBody?: OpenAICompat["extraBody"];
|
|
476
461
|
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
@@ -489,7 +474,6 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
489
474
|
supportsLongPromptCacheRetention: boolean;
|
|
490
475
|
strictResponsesPairing: boolean;
|
|
491
476
|
supportsImageDetailOriginal: boolean;
|
|
492
|
-
requiresJuiceZeroHack: boolean;
|
|
493
477
|
supportsObfuscationOptOut: boolean;
|
|
494
478
|
streamIdleTimeoutMs?: number;
|
|
495
479
|
}
|
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.2
|
|
4
|
+
"version": "16.3.2",
|
|
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.2
|
|
37
|
+
"@oh-my-pi/pi-utils": "16.3.2",
|
|
38
38
|
"arktype": "^2.2.0",
|
|
39
39
|
"zod": "^4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "16.2
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.3.2",
|
|
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,6 @@ 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 juice-zero hack keys on the model name, matching the historical
|
|
578
|
-
* request-time check.
|
|
579
596
|
*/
|
|
580
597
|
export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
|
|
581
598
|
const baseUrl = spec.baseUrl ?? "";
|
|
@@ -607,7 +624,6 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
607
624
|
// or base-URL host (mirroring the Anthropic compat builder) so a model
|
|
608
625
|
// pointed at the Copilot host under a different provider id still clamps.
|
|
609
626
|
supportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
|
|
610
|
-
requiresJuiceZeroHack: spec.name.toLowerCase().startsWith("gpt-5"),
|
|
611
627
|
reasoningEffortMap: {},
|
|
612
628
|
supportsReasoningParams: true,
|
|
613
629
|
thinkingFormat,
|
|
@@ -646,7 +662,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
646
662
|
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
|
647
663
|
stripDeepseekSpecialTokens:
|
|
648
664
|
Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
|
|
649
|
-
streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id) : undefined,
|
|
665
|
+
streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,
|
|
650
666
|
reasoningDeltasMayBeCumulative:
|
|
651
667
|
MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
|
|
652
668
|
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
|
@@ -674,7 +690,6 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
|
|
|
674
690
|
supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
|
|
675
691
|
strictResponsesPairing: compat.strictResponsesPairing,
|
|
676
692
|
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
|
|
677
|
-
requiresJuiceZeroHack: compat.requiresJuiceZeroHack,
|
|
678
693
|
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
|
679
694
|
} satisfies ResponsesOnlyCompat;
|
|
680
695
|
}
|
|
@@ -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;
|
|
@@ -81,7 +82,7 @@ export interface CodexModelDiscoveryResult {
|
|
|
81
82
|
* Returns `{ models: [] }` when a route succeeds but yields no usable models.
|
|
82
83
|
*/
|
|
83
84
|
export async function fetchCodexModels(options: CodexModelDiscoveryOptions): Promise<CodexModelDiscoveryResult | null> {
|
|
84
|
-
const fetchFn = options.fetchFn
|
|
85
|
+
const fetchFn = discoveryFetch(options.fetchFn);
|
|
85
86
|
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
86
87
|
const paths = normalizePaths(options.paths);
|
|
87
88
|
const headers = buildCodexHeaders(options);
|
|
@@ -168,7 +169,7 @@ function buildCodexHeaders(options: CodexModelDiscoveryOptions): Headers {
|
|
|
168
169
|
|
|
169
170
|
async function resolveCodexClientVersion(
|
|
170
171
|
clientVersion: string | undefined,
|
|
171
|
-
fetchFn:
|
|
172
|
+
fetchFn: FetchImpl,
|
|
172
173
|
signal: AbortSignal | undefined,
|
|
173
174
|
): Promise<string> {
|
|
174
175
|
const normalizedClientVersion = normalizeClientVersion(clientVersion);
|
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
|
|
|
@@ -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" },
|
|
@@ -453,7 +453,8 @@ export const CATALOG_PROVIDERS = [
|
|
|
453
453
|
defaultModel: "anthropic/claude-opus-4.8",
|
|
454
454
|
envVars: ["ZENMUX_API_KEY"],
|
|
455
455
|
createModelManagerOptions: (config: ModelManagerConfig) => zenmuxModelManagerOptions(config),
|
|
456
|
-
|
|
456
|
+
allowUnauthenticated: true,
|
|
457
|
+
catalogDiscovery: { label: "ZenMux", allowUnauthenticated: true },
|
|
457
458
|
},
|
|
458
459
|
{
|
|
459
460
|
id: "zhipu-coding-plan",
|
|
@@ -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++) {
|
|
@@ -2035,7 +2035,11 @@ function normalizeZenMuxOpenAiBaseUrl(baseUrl?: string): string {
|
|
|
2035
2035
|
if (!value) {
|
|
2036
2036
|
return ZENMUX_OPENAI_BASE_URL;
|
|
2037
2037
|
}
|
|
2038
|
-
|
|
2038
|
+
const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2039
|
+
if (normalized.endsWith("/api/anthropic")) {
|
|
2040
|
+
return normalized.replace("/api/anthropic", "/api/v1");
|
|
2041
|
+
}
|
|
2042
|
+
return normalized;
|
|
2039
2043
|
}
|
|
2040
2044
|
|
|
2041
2045
|
function toZenMuxAnthropicBaseUrl(openAiBaseUrl: string): string {
|
|
@@ -2103,37 +2107,35 @@ export function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): Mo
|
|
|
2103
2107
|
const anthropicBaseUrl = toZenMuxAnthropicBaseUrl(openAiBaseUrl);
|
|
2104
2108
|
return {
|
|
2105
2109
|
providerId: "zenmux",
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
}),
|
|
2136
|
-
}),
|
|
2110
|
+
fetchDynamicModels: () =>
|
|
2111
|
+
fetchOpenAICompatibleModels<Api>({
|
|
2112
|
+
api: "openai-completions",
|
|
2113
|
+
provider: "zenmux",
|
|
2114
|
+
baseUrl: openAiBaseUrl,
|
|
2115
|
+
apiKey,
|
|
2116
|
+
mapModel: (entry, defaults) => {
|
|
2117
|
+
const pricings = isRecord(entry.pricings) ? entry.pricings : undefined;
|
|
2118
|
+
const capabilities = isRecord(entry.capabilities) ? entry.capabilities : undefined;
|
|
2119
|
+
const isAnthropicModel = isZenMuxAnthropicModel(entry, defaults.id);
|
|
2120
|
+
return {
|
|
2121
|
+
...defaults,
|
|
2122
|
+
name: toModelName(entry.display_name, defaults.name),
|
|
2123
|
+
api: isAnthropicModel ? "anthropic-messages" : "openai-completions",
|
|
2124
|
+
baseUrl: isAnthropicModel ? anthropicBaseUrl : openAiBaseUrl,
|
|
2125
|
+
reasoning: capabilities?.reasoning === true || defaults.reasoning,
|
|
2126
|
+
input: toInputCapabilities(entry.input_modalities),
|
|
2127
|
+
cost: {
|
|
2128
|
+
input: getZenMuxPricingValue(pricings, "prompt"),
|
|
2129
|
+
output: getZenMuxPricingValue(pricings, "completion"),
|
|
2130
|
+
cacheRead: getZenMuxPricingValue(pricings, "input_cache_read"),
|
|
2131
|
+
cacheWrite: getZenMuxCacheWritePrice(pricings),
|
|
2132
|
+
},
|
|
2133
|
+
contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
|
|
2134
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
|
|
2135
|
+
};
|
|
2136
|
+
},
|
|
2137
|
+
fetch: config?.fetch,
|
|
2138
|
+
}),
|
|
2137
2139
|
};
|
|
2138
2140
|
}
|
|
2139
2141
|
|
|
@@ -2926,6 +2928,33 @@ function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
|
|
|
2926
2928
|
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2927
2929
|
}
|
|
2928
2930
|
|
|
2931
|
+
const LITELLM_RESELLER_USAGE_SUFFIX = /\s+\(\d+(?:\.\d+)?[x×] usage\)$/i;
|
|
2932
|
+
|
|
2933
|
+
function stripLiteLLMResellerUsageSuffix(name: string): string {
|
|
2934
|
+
const cleaned = name.replace(LITELLM_RESELLER_USAGE_SUFFIX, "").trim();
|
|
2935
|
+
return cleaned.length > 0 ? cleaned : name;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function toLiteLLMDisplayName(modelName: string | undefined, referenceName: string | undefined, id: string): string {
|
|
2939
|
+
const cleanedModelName = modelName ? stripLiteLLMResellerUsageSuffix(modelName) : undefined;
|
|
2940
|
+
if (cleanedModelName && cleanedModelName !== id) {
|
|
2941
|
+
return cleanedModelName;
|
|
2942
|
+
}
|
|
2943
|
+
return referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
function mapLiteLLMOpenAICompatibleModel<TApi extends Api>(
|
|
2947
|
+
entry: OpenAICompatibleModelRecord,
|
|
2948
|
+
defaults: ModelSpec<TApi>,
|
|
2949
|
+
reference: ModelSpec<TApi> | undefined,
|
|
2950
|
+
): ModelSpec<TApi> {
|
|
2951
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2952
|
+
return {
|
|
2953
|
+
...model,
|
|
2954
|
+
name: stripLiteLLMResellerUsageSuffix(model.name),
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2929
2958
|
function toNonEmptyString(value: unknown): string | undefined {
|
|
2930
2959
|
if (typeof value !== "string") {
|
|
2931
2960
|
return undefined;
|
|
@@ -3025,7 +3054,7 @@ function mapLiteLLMRichEntry<TApi extends Api>(
|
|
|
3025
3054
|
};
|
|
3026
3055
|
return {
|
|
3027
3056
|
id,
|
|
3028
|
-
name: modelName
|
|
3057
|
+
name: toLiteLLMDisplayName(modelName, reference?.name, id),
|
|
3029
3058
|
api: options.api,
|
|
3030
3059
|
provider: options.provider,
|
|
3031
3060
|
baseUrl: runtimeBaseUrl,
|
|
@@ -3052,7 +3081,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
3052
3081
|
runtimeBaseUrl: string,
|
|
3053
3082
|
signal?: AbortSignal,
|
|
3054
3083
|
): Promise<ModelSpec<TApi>[] | null> {
|
|
3055
|
-
const fetchImpl = options.fetch
|
|
3084
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
3056
3085
|
const requestHeaders: Record<string, string> = {
|
|
3057
3086
|
Accept: "application/json",
|
|
3058
3087
|
...options.headers,
|
|
@@ -3126,7 +3155,11 @@ export function litellmModelManagerOptions(
|
|
|
3126
3155
|
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
3127
3156
|
return {
|
|
3128
3157
|
providerId: "litellm",
|
|
3129
|
-
|
|
3158
|
+
// rich-v2 invalidates rows cached before reseller usage-suffix stripping
|
|
3159
|
+
// (stale display names like `MiniMax-M3 (3x usage)`); bump the version
|
|
3160
|
+
// whenever the mappers below change, or warm authoritative caches keep
|
|
3161
|
+
// serving pre-change rows for the full TTL.
|
|
3162
|
+
cacheProviderId: `litellm:rich-v2:${Bun.hash(baseUrl).toString(36)}`,
|
|
3130
3163
|
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3131
3164
|
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3132
3165
|
// management metadata, then fall back to /v1/models and enrich bare ids
|
|
@@ -3151,7 +3184,8 @@ export function litellmModelManagerOptions(
|
|
|
3151
3184
|
provider: "litellm",
|
|
3152
3185
|
baseUrl,
|
|
3153
3186
|
apiKey,
|
|
3154
|
-
mapModel: (entry, defaults) =>
|
|
3187
|
+
mapModel: (entry, defaults) =>
|
|
3188
|
+
mapLiteLLMOpenAICompatibleModel(entry, defaults, resolveReference(defaults.id)),
|
|
3155
3189
|
fetch: config?.fetch,
|
|
3156
3190
|
});
|
|
3157
3191
|
},
|
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
|
|
|
@@ -331,14 +324,6 @@ export interface OpenAICompat {
|
|
|
331
324
|
strictResponsesPairing?: boolean;
|
|
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
|
-
/**
|
|
335
|
-
* Append a trailing `# Juice: 0 !important` developer item when the caller
|
|
336
|
-
* did not request reasoning, suppressing default reasoning on models that
|
|
337
|
-
* cannot disable it via request params (Responses APIs only; see
|
|
338
|
-
* https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
|
|
339
|
-
* Default: auto-detected (GPT-5-family model names).
|
|
340
|
-
*/
|
|
341
|
-
requiresJuiceZeroHack?: boolean;
|
|
342
327
|
/** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
|
|
343
328
|
reasoningDeltasMayBeCumulative?: boolean;
|
|
344
329
|
/** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
|
|
@@ -564,7 +549,6 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
564
549
|
| "thinkingKeep"
|
|
565
550
|
| "strictResponsesPairing"
|
|
566
551
|
| "supportsImageDetailOriginal"
|
|
567
|
-
| "requiresJuiceZeroHack"
|
|
568
552
|
| "enableGeminiThinkingLoopGuard"
|
|
569
553
|
| "whenThinking"
|
|
570
554
|
>
|
|
@@ -588,7 +572,6 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
588
572
|
supportsLongPromptCacheRetention: boolean;
|
|
589
573
|
strictResponsesPairing: boolean;
|
|
590
574
|
supportsImageDetailOriginal: boolean;
|
|
591
|
-
requiresJuiceZeroHack: boolean;
|
|
592
575
|
supportsObfuscationOptOut: boolean;
|
|
593
576
|
streamIdleTimeoutMs?: number;
|
|
594
577
|
}
|
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
|
}
|