@oh-my-pi/pi-catalog 16.2.13 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.0] - 2026-07-02
6
+
7
+ ### Breaking Changes
8
+
9
+ - Renamed the `requiresJuiceZeroHack` compatibility flag to `requiresReasoningSuppressionPrompt` (affecting `OpenAICompat` and `ResolvedOpenAIResponsesCompat`) and removed the unused `"juice-zero-developer-message"` member from `OpenAIReasoningDisableMode`.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed stream markup healing pattern misfires by disabling the healer on the official OpenAI endpoint.
14
+ - Updated the Xiaomi provider's default model to the supported `mimo-v2.5` model.
15
+ - Fixed model discovery probes (including Ollama and metadata fetches) failing behind private-CA gateways by ensuring they honor the `NODE_EXTRA_CA_CERTS` environment variable.
16
+ - Fixed CoreWeave Serverless Inference project-header detection to ensure blank OpenAI-Project overrides do not block the `COREWEAVE_PROJECT` fallback.
17
+ - Fixed LiteLLM MiniMax M3 discovery to remove reseller-only display suffixes and invalidated the model cache to clear stale suffixes immediately.
18
+ - Fixed ZenMux's `anthropic-messages` proxy being misclassified as a non-signing reasoning endpoint (`replayUnsignedThinking: true`), matching the GitHub Copilot fix (#2851). ZenMux's `zenmux.ai/api/anthropic` route forwards to signature-enforcing Anthropic, so replaying a stripped/unsigned historical `thinking` block as `signature: ""` — most visibly an end_turn-bound checkpoint/branch-return turn whose signature the transform must strip — caused `400 messages.1.content.0: Invalid signature in thinking` on Claude Sonnet 5 and other reasoning models. ([#4192](https://github.com/can1357/oh-my-pi/issues/4192))
19
+
5
20
  ## [16.2.13] - 2026-07-01
6
21
 
7
22
  ### Added
@@ -184,7 +199,7 @@
184
199
 
185
200
  ### Fixed
186
201
 
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))
202
+ - Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067))
188
203
 
189
204
  ## [16.1.3] - 2026-06-19
190
205
 
@@ -20,8 +20,8 @@ interface OpenAIResponsesSpecLike {
20
20
  * as well as URL — bundled `azure` models carry no baseUrl (the deployment host
21
21
  * is per-resource, resolved at runtime) — while OpenAI/Copilot developer-role
22
22
  * and prompt-cache detection stay URL-keyed, as the historical call sites were.
23
- * The GPT-5 juice-zero hack keys on the model name, matching the historical
24
- * request-time check.
23
+ * The GPT-5 reasoning-suppression prompt keys on the model name, matching the
24
+ * historical request-time check.
25
25
  */
26
26
  export declare function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat;
27
27
  export declare function buildOpenRouterCompat(spec: ModelSpec<"openrouter">): ResolvedOpenRouterCompat;
@@ -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: {
@@ -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.
@@ -294,13 +287,13 @@ export interface OpenAICompat {
294
287
  /** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
295
288
  supportsImageDetailOriginal?: boolean;
296
289
  /**
297
- * Append a trailing `# 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
290
+ * Append a trailing no-reasoning developer item when the caller did not
291
+ * request reasoning, suppressing default reasoning on models that cannot
292
+ * disable it via request params (Responses APIs only; see
300
293
  * https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
301
- * Default: auto-detected (GPT-5-family model names).
294
+ * The prompt must not look like an execution or tool budget. Default: auto-detected (GPT-5-family model names).
302
295
  */
303
- requiresJuiceZeroHack?: boolean;
296
+ requiresReasoningSuppressionPrompt?: boolean;
304
297
  /** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
305
298
  reasoningDeltasMayBeCumulative?: boolean;
306
299
  /** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
@@ -470,7 +463,7 @@ export interface ResolvedOpenAISharedCompat {
470
463
  * `buildModel`; request handlers read fields and never detect, resolve, or
471
464
  * allocate.
472
465
  */
473
- export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresJuiceZeroHack" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
466
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "requiresReasoningSuppressionPrompt" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
474
467
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
475
468
  extraBody?: OpenAICompat["extraBody"];
476
469
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -489,7 +482,7 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
489
482
  supportsLongPromptCacheRetention: boolean;
490
483
  strictResponsesPairing: boolean;
491
484
  supportsImageDetailOriginal: boolean;
492
- requiresJuiceZeroHack: boolean;
485
+ requiresReasoningSuppressionPrompt: boolean;
493
486
  supportsObfuscationOptOut: boolean;
494
487
  streamIdleTimeoutMs?: number;
495
488
  }
@@ -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.0",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "@oh-my-pi/pi-utils": "16.2.13",
37
+ "@oh-my-pi/pi-utils": "16.3.0",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.2.13",
42
+ "@oh-my-pi/pi-ai": "16.3.0",
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,8 @@ interface OpenAIResponsesSpecLike {
574
593
  * as well as URL — bundled `azure` models carry no baseUrl (the deployment host
575
594
  * is per-resource, resolved at runtime) — while OpenAI/Copilot developer-role
576
595
  * and prompt-cache detection stay URL-keyed, as the historical call sites were.
577
- * The GPT-5 juice-zero hack keys on the model name, matching the historical
578
- * request-time check.
596
+ * The GPT-5 reasoning-suppression prompt keys on the model name, matching the
597
+ * historical request-time check.
579
598
  */
580
599
  export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
581
600
  const baseUrl = spec.baseUrl ?? "";
@@ -607,7 +626,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
607
626
  // or base-URL host (mirroring the Anthropic compat builder) so a model
608
627
  // pointed at the Copilot host under a different provider id still clamps.
609
628
  supportsImageDetailOriginal: !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
610
- requiresJuiceZeroHack: spec.name.toLowerCase().startsWith("gpt-5"),
629
+ requiresReasoningSuppressionPrompt: spec.name.toLowerCase().startsWith("gpt-5"),
611
630
  reasoningEffortMap: {},
612
631
  supportsReasoningParams: true,
613
632
  thinkingFormat,
@@ -646,7 +665,7 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
646
665
  supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
647
666
  stripDeepseekSpecialTokens:
648
667
  Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
649
- streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id) : undefined,
668
+ streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id, baseUrl) : undefined,
650
669
  reasoningDeltasMayBeCumulative:
651
670
  MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
652
671
  emptyLengthFinishIsContextError: spec.provider === "ollama",
@@ -674,7 +693,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
674
693
  supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
675
694
  strictResponsesPairing: compat.strictResponsesPairing,
676
695
  supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
677
- requiresJuiceZeroHack: compat.requiresJuiceZeroHack,
696
+ requiresReasoningSuppressionPrompt: compat.requiresReasoningSuppressionPrompt,
678
697
  supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
679
698
  } satisfies ResponsesOnlyCompat;
680
699
  }
@@ -1,6 +1,6 @@
1
1
  import { type } from "arktype";
2
2
  import type { ModelSpec } from "../types";
3
- import { toPositiveNumber } from "../utils";
3
+ import { discoveryFetch, toPositiveNumber } from "../utils";
4
4
  import {
5
5
  ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
6
6
  collapseEffortVariants,
@@ -161,7 +161,7 @@ export interface FetchAntigravityDiscoveryModelsOptions {
161
161
  export async function fetchAntigravityDiscoveryModels(
162
162
  options: FetchAntigravityDiscoveryModelsOptions,
163
163
  ): Promise<ModelSpec<"google-gemini-cli">[] | null> {
164
- const fetcher = options.fetcher ?? 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" },
@@ -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++) {
@@ -2926,6 +2926,33 @@ function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
2926
2926
  return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
2927
2927
  }
2928
2928
 
2929
+ const LITELLM_RESELLER_USAGE_SUFFIX = /\s+\(\d+(?:\.\d+)?[x×] usage\)$/i;
2930
+
2931
+ function stripLiteLLMResellerUsageSuffix(name: string): string {
2932
+ const cleaned = name.replace(LITELLM_RESELLER_USAGE_SUFFIX, "").trim();
2933
+ return cleaned.length > 0 ? cleaned : name;
2934
+ }
2935
+
2936
+ function toLiteLLMDisplayName(modelName: string | undefined, referenceName: string | undefined, id: string): string {
2937
+ const cleanedModelName = modelName ? stripLiteLLMResellerUsageSuffix(modelName) : undefined;
2938
+ if (cleanedModelName && cleanedModelName !== id) {
2939
+ return cleanedModelName;
2940
+ }
2941
+ return referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;
2942
+ }
2943
+
2944
+ function mapLiteLLMOpenAICompatibleModel<TApi extends Api>(
2945
+ entry: OpenAICompatibleModelRecord,
2946
+ defaults: ModelSpec<TApi>,
2947
+ reference: ModelSpec<TApi> | undefined,
2948
+ ): ModelSpec<TApi> {
2949
+ const model = mapWithBundledReference(entry, defaults, reference);
2950
+ return {
2951
+ ...model,
2952
+ name: stripLiteLLMResellerUsageSuffix(model.name),
2953
+ };
2954
+ }
2955
+
2929
2956
  function toNonEmptyString(value: unknown): string | undefined {
2930
2957
  if (typeof value !== "string") {
2931
2958
  return undefined;
@@ -3025,7 +3052,7 @@ function mapLiteLLMRichEntry<TApi extends Api>(
3025
3052
  };
3026
3053
  return {
3027
3054
  id,
3028
- name: modelName && modelName !== id ? modelName : (reference?.name ?? id),
3055
+ name: toLiteLLMDisplayName(modelName, reference?.name, id),
3029
3056
  api: options.api,
3030
3057
  provider: options.provider,
3031
3058
  baseUrl: runtimeBaseUrl,
@@ -3052,7 +3079,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
3052
3079
  runtimeBaseUrl: string,
3053
3080
  signal?: AbortSignal,
3054
3081
  ): Promise<ModelSpec<TApi>[] | null> {
3055
- const fetchImpl = options.fetch ?? globalThis.fetch;
3082
+ const fetchImpl = discoveryFetch(options.fetch);
3056
3083
  const requestHeaders: Record<string, string> = {
3057
3084
  Accept: "application/json",
3058
3085
  ...options.headers,
@@ -3126,7 +3153,11 @@ export function litellmModelManagerOptions(
3126
3153
  const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
3127
3154
  return {
3128
3155
  providerId: "litellm",
3129
- cacheProviderId: `litellm:rich-v1:${Bun.hash(baseUrl).toString(36)}`,
3156
+ // rich-v2 invalidates rows cached before reseller usage-suffix stripping
3157
+ // (stale display names like `MiniMax-M3 (3x usage)`); bump the version
3158
+ // whenever the mappers below change, or warm authoritative caches keep
3159
+ // serving pre-change rows for the full TTL.
3160
+ cacheProviderId: `litellm:rich-v2:${Bun.hash(baseUrl).toString(36)}`,
3130
3161
  // litellm is a local-only proxy and is never bundled in models.json (that
3131
3162
  // would leak the machine's localhost catalog). Prefer the proxy's richer
3132
3163
  // management metadata, then fall back to /v1/models and enrich bare ids
@@ -3151,7 +3182,8 @@ export function litellmModelManagerOptions(
3151
3182
  provider: "litellm",
3152
3183
  baseUrl,
3153
3184
  apiKey,
3154
- mapModel: (entry, defaults) => mapWithBundledReference(entry, defaults, resolveReference(defaults.id)),
3185
+ mapModel: (entry, defaults) =>
3186
+ mapLiteLLMOpenAICompatibleModel(entry, defaults, resolveReference(defaults.id)),
3155
3187
  fetch: config?.fetch,
3156
3188
  });
3157
3189
  },
package/src/types.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Effort } from "./effort";
2
2
 
3
+ // Re-exported from @oh-my-pi/pi-utils so the whole workspace shares one
4
+ // `fetch`-compatible signature (tls-fetch's wrappers produce/accept it).
5
+ export type { FetchImpl } from "@oh-my-pi/pi-utils";
3
6
  export type { KnownProvider } from "./provider-models/descriptors";
4
7
 
5
8
  export type KnownApi =
@@ -89,15 +92,6 @@ export type Provider = string;
89
92
  /** Token budgets for each thinking level (token-based providers only) */
90
93
  export type ThinkingBudgets = { [key in Effort]?: number };
91
94
 
92
- /**
93
- * `fetch`-compatible function. Accepts any callable matching the standard
94
- * fetch signature; `preconnect` is optional because non-Bun runtimes (browsers,
95
- * test mocks) won't expose it.
96
- */
97
- export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
98
- preconnect?: typeof globalThis.fetch.preconnect;
99
- };
100
-
101
95
  export interface Usage {
102
96
  /** Non-cached input tokens (matches the bucket the provider bills as new input). */
103
97
  input: number;
@@ -153,8 +147,7 @@ export type OpenAIReasoningDisableMode =
153
147
  | "openrouter-enabled-false"
154
148
  | "zai-thinking-disabled"
155
149
  | "qwen-enable-thinking-false"
156
- | "qwen-template-false"
157
- | "juice-zero-developer-message";
150
+ | "qwen-template-false";
158
151
 
159
152
  export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
160
153
 
@@ -332,13 +325,13 @@ export interface OpenAICompat {
332
325
  /** Whether the Responses API accepts the `detail: "original"` image hint. Default: auto-detected (false for GitHub Copilot, which rejects it with a 400). */
333
326
  supportsImageDetailOriginal?: boolean;
334
327
  /**
335
- * Append a trailing `# 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
328
+ * Append a trailing no-reasoning developer item when the caller did not
329
+ * request reasoning, suppressing default reasoning on models that cannot
330
+ * disable it via request params (Responses APIs only; see
338
331
  * https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7).
339
- * Default: auto-detected (GPT-5-family model names).
332
+ * The prompt must not look like an execution or tool budget. Default: auto-detected (GPT-5-family model names).
340
333
  */
341
- requiresJuiceZeroHack?: boolean;
334
+ requiresReasoningSuppressionPrompt?: boolean;
342
335
  /** Whether streamed reasoning deltas for the same field may repeat the full cumulative text snapshot. Default: false. */
343
336
  reasoningDeltasMayBeCumulative?: boolean;
344
337
  /** Strip leaked DeepSeek chat-template special tokens from visible content deltas. Default: auto-detected. */
@@ -564,7 +557,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
564
557
  | "thinkingKeep"
565
558
  | "strictResponsesPairing"
566
559
  | "supportsImageDetailOriginal"
567
- | "requiresJuiceZeroHack"
560
+ | "requiresReasoningSuppressionPrompt"
568
561
  | "enableGeminiThinkingLoopGuard"
569
562
  | "whenThinking"
570
563
  >
@@ -588,7 +581,7 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
588
581
  supportsLongPromptCacheRetention: boolean;
589
582
  strictResponsesPairing: boolean;
590
583
  supportsImageDetailOriginal: boolean;
591
- requiresJuiceZeroHack: boolean;
584
+ requiresReasoningSuppressionPrompt: boolean;
592
585
  supportsObfuscationOptOut: boolean;
593
586
  streamIdleTimeoutMs?: number;
594
587
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,15 @@
1
+ import { type FetchImpl, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils";
2
+
1
3
  export { isRecord } from "@oh-my-pi/pi-utils";
2
4
 
5
+ /**
6
+ * Fetch implementation for catalog discovery probes: the caller's override
7
+ * when given, otherwise global fetch wrapped for `NODE_EXTRA_CA_CERTS`.
8
+ */
9
+ export function discoveryFetch(override?: FetchImpl): FetchImpl {
10
+ return override ?? wrapFetchForExtraCa(fetch);
11
+ }
12
+
3
13
  export function toNumber(value: unknown): number | undefined {
4
14
  if (typeof value === "number" && Number.isFinite(value)) {
5
15
  return value;
@@ -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
  }