@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 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 that declare the dead ids as `retiredMembers` so `reconcileRetiredRouting` re-points stale bundled-catalog and SQLite-cache rows away from the 404 wire id. Refreshed the bundled `models.json` Sonnet 4.6 entry whose stored `effortRouting` still targeted the dead `-thinking` 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))
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;
@@ -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-flash";
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";
@@ -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" | "juice-zero-developer-message";
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" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
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
  }
@@ -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.13",
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.13",
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.13",
42
+ "@oh-my-pi/pi-ai": "16.3.2",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -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 Anthropic-compatible proxy (api.githubcopilot.com/v1/messages)
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 is excluded: it forwards to
81
- // signature-enforcing Anthropic and returns full thinking signatures, so it
82
- // is a SIGNING endpoint. Replaying a stripped/unsigned thinking block as
83
- // `signature: ""` there 400s the whole request ("Invalid signature") — most
84
- // visibly when a checkpoint/branch-return turn's end_turn-bound signature is
85
- // stripped on replay (issue #2851). Treating it like official Anthropic
86
- // degrades such blocks to text instead, which the API accepts.
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 && (isZai || modelMatchesHost(spec, "deepseekFamily") || (spec.reasoning && !official)),
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);
@@ -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; every other model defaults to `"thinking"`. All patterns
121
- * run the generic thinking healer, so leaked reasoning idioms (e.g. a Gemini
122
- * ` ```thinking ` fence on OpenRouter) are always recovered from `delta.content`.
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(provider: string, modelId: string): OpenAIStreamMarkupHealingPattern {
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 ?? fetch;
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);
@@ -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 ?? fetch;
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: typeof fetch,
172
+ fetchFn: FetchImpl,
172
173
  signal: AbortSignal | undefined,
173
174
  ): Promise<string> {
174
175
  const normalizedClientVersion = normalizeClientVersion(clientVersion);
@@ -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 ?? 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;
@@ -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 ?? 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 ?? 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 ?? 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 ?? 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 ?? 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 ?? globalThis.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-flash",
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
- catalogDiscovery: { label: "ZenMux" },
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 = fetch,
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 = fetch): Promise<unknown> {
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 = fetch,
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 = fetch,
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 ?? 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 ?? 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
- return value.endsWith("/") ? value.slice(0, -1) : value;
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
- ...(apiKey && {
2107
- fetchDynamicModels: () =>
2108
- fetchOpenAICompatibleModels<Api>({
2109
- api: "openai-completions",
2110
- provider: "zenmux",
2111
- baseUrl: openAiBaseUrl,
2112
- apiKey,
2113
- mapModel: (entry, defaults) => {
2114
- const pricings = isRecord(entry.pricings) ? entry.pricings : undefined;
2115
- const capabilities = isRecord(entry.capabilities) ? entry.capabilities : undefined;
2116
- const isAnthropicModel = isZenMuxAnthropicModel(entry, defaults.id);
2117
- return {
2118
- ...defaults,
2119
- name: toModelName(entry.display_name, defaults.name),
2120
- api: isAnthropicModel ? "anthropic-messages" : "openai-completions",
2121
- baseUrl: isAnthropicModel ? anthropicBaseUrl : openAiBaseUrl,
2122
- reasoning: capabilities?.reasoning === true || defaults.reasoning,
2123
- input: toInputCapabilities(entry.input_modalities),
2124
- cost: {
2125
- input: getZenMuxPricingValue(pricings, "prompt"),
2126
- output: getZenMuxPricingValue(pricings, "completion"),
2127
- cacheRead: getZenMuxPricingValue(pricings, "input_cache_read"),
2128
- cacheWrite: getZenMuxCacheWritePrice(pricings),
2129
- },
2130
- contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
2131
- maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
2132
- };
2133
- },
2134
- fetch: config?.fetch,
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 && modelName !== id ? modelName : (reference?.name ?? id),
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 ?? globalThis.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
- cacheProviderId: `litellm:rich-v1:${Bun.hash(baseUrl).toString(36)}`,
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) => mapWithBundledReference(entry, defaults, resolveReference(defaults.id)),
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;
@@ -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.keys(headers).some(header => header.toLowerCase() === normalized);
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
  }