@oh-my-pi/pi-catalog 17.2.15 → 17.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,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.3.0] - 2026-08-13
6
+
7
+ ### Breaking Changes
8
+
9
+ - Removed `OpenAICompat.enableGeminiThinkingLoopGuard`; thinking-loop eligibility is derived solely from the `model.id` family.
10
+
11
+ ### Added
12
+
13
+ - Added first-party OpenAI Daybreak Blue, Daybreak Red, and GPT-5.6 Cyber models with full support for their documented API pricing (including long-context rates above 272K input), token limits, tools, and reasoning effort controls (off/low/medium/high/xhigh/max).
14
+ - Added calculateUncachedInputCost() to calculate prompt pricing against active context-length tiers without prompt caching.
15
+
16
+ ### Fixed
17
+
18
+ - Fixed Anthropic cache-write pricing to correctly honor mixed 5-minute and 1-hour TTL usage instead of incorrectly charging all writes at the 5-minute rate.
19
+ - Fixed Ollama Cloud DeepSeek V4 Flash and older reasoners to correctly apply the DeepSeek effort contract (e.g., low/high/max) instead of the generic effort ladder.
20
+ - Added a default request timeout to OpenAI-compatible model discovery to prevent stalled provider endpoints from hanging startup indefinitely.
21
+ - Fixed Anthropic cache-write pricing to honor mixed 5-minute and 1-hour TTL usage instead of charging every write at the 5-minute rate.
22
+ - Fixed Ollama Cloud DeepSeek V4 Flash (including dated/preview tags like `deepseek-v4-flash:0731`) exposing the generic `minimal`/`low`/`medium`/`high`/`xhigh` effort ladder without `max`; the `ollama-chat` transport now applies the DeepSeek effort contract (Flash → `low`/`high`/`max`, older reasoners → `high`/`max`), matching the direct API and every other host ([#8334](https://github.com/can1357/oh-my-pi/issues/8334)).
23
+ - Exposed the `low` reasoning-effort tier for DeepSeek V4 Pro on the direct API and faithful aggregator routes, matching DeepSeek's updated API contract advertising `reasoning_effort` `low`/`high`/`max` for both V4 SKUs; OpenRouter's non-Flash route still exposes only `high`, and the older V3.x/R1 reasoners remain `high`/`max` ([#8405](https://github.com/can1357/oh-my-pi/issues/8405)).
24
+ - Bounded OpenAI-compatible model discovery with a default request timeout so a stalled provider `/models` endpoint can no longer hang startup indefinitely in `resolveModelDiscoveryFallback` ([#8315](https://github.com/can1357/oh-my-pi/issues/8315)).
25
+ - Fixed Codex-discovered `gpt-daybreak-*` aliases being treated as unknown models, restoring the GPT-5.6 `low`/`medium`/`high`/`xhigh`/`max` effort ladder and its 372K fallback only when the Codex registry omits `context_window`.
26
+ - Fixed first-party OpenAI GPT-5.6 aliases to preserve wire-level `off` through generated pro aliases and to price requests above 272K input at each SKU's documented long-context rates.
27
+
5
28
  ## [17.2.15] - 2026-08-12
6
29
 
7
30
  ### Fixed
@@ -1,4 +1,15 @@
1
1
  import type { Api, FetchImpl, ModelSpec, Provider } from "../types.js";
2
+ /**
3
+ * Default hard deadline applied to an OpenAI-compatible `/models` probe when
4
+ * the caller supplies neither an `AbortSignal` nor an explicit `timeoutMs`.
5
+ *
6
+ * Built-in provider model managers (openrouter, xAI, DeepSeek, …) call
7
+ * {@link fetchOpenAICompatibleModels} with no timeout, so without this bound a
8
+ * stalled endpoint left the request pending forever and blocked startup's
9
+ * awaited `resolveModelDiscoveryFallback` discovery pass indefinitely
10
+ * (issue #8315). 10s matches the coding-agent's remote-discovery budget.
11
+ */
12
+ export declare const DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS = 10000;
2
13
  /**
3
14
  * Minimal OpenAI-style model entry shape consumed by discovery.
4
15
  *
@@ -50,7 +61,11 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
50
61
  headers?: Record<string, string>;
51
62
  /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
52
63
  signal?: AbortSignal;
53
- /** Optional cancellable request timeout used when `signal` is omitted. */
64
+ /**
65
+ * Optional cancellable request timeout used when `signal` is omitted.
66
+ * Defaults to {@link DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS} so a
67
+ * stalled endpoint can never hang discovery indefinitely.
68
+ */
54
69
  timeoutMs?: number;
55
70
  /** Optional fetch implementation override for testing/custom runtimes. */
56
71
  fetch?: FetchImpl;
@@ -36,13 +36,19 @@ export declare const isGemmaModelId: (modelId: string) => boolean;
36
36
  export declare const isDeepseekModelIdOrName: (modelId: string) => boolean;
37
37
  /**
38
38
  * DeepSeek V4 Flash SKU in any host/namespace form (`deepseek-v4-flash`, dated
39
- * `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Flash is the only
40
- * V4 model whose `reasoning_effort` accepts the `low` tier; V4 Pro tops out at
41
- * `high`/`max`. See https://api-docs.deepseek.com/api/create-chat-completion.
39
+ * `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Both V4 SKUs
40
+ * (Flash and Pro) accept the `low` reasoning_effort tier; this predicate keeps
41
+ * Flash distinguishable from Pro where a host quirk splits them (e.g.
42
+ * OpenRouter exposes `low` on Flash but only `high` on non-Flash V4).
43
+ * See https://api-docs.deepseek.com/api/create-chat-completion.
42
44
  */
43
45
  export declare const isDeepseekV4FlashModelId: (modelId: string) => boolean;
44
46
  /** Xiaomi MiMo family by id or display name. */
45
47
  export declare const isMimoModelIdOrName: (modelId: string) => boolean;
48
+ /** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
49
+ export declare const isGeminiModelId: (modelId: string) => boolean;
50
+ /** Grok family ids across namespace and delimiter forms (`grok-*`, `cursor-grok-*`, `xai/grok-*`). */
51
+ export declare const isGrokModelId: (modelId: string) => boolean;
46
52
  /**
47
53
  * Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
48
54
  * (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
@@ -4,6 +4,8 @@ export type GeneratedProvider = keyof typeof MODELS;
4
4
  export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
5
5
  export declare function getBundledProviders(): KnownProvider[];
6
6
  export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
7
+ /** Price a prompt as fully uncached input under its active context-length tier. */
8
+ export declare function calculateUncachedInputCost(cost: Model["cost"], promptInputTokens: number): number;
7
9
  export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"];
8
10
  /**
9
11
  * Check if two models are equal by comparing both their id and provider.
@@ -61,12 +61,41 @@ export interface UmansModelManagerConfig {
61
61
  fetch?: FetchImpl;
62
62
  }
63
63
  export declare function umansModelManagerOptions(config?: UmansModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
64
+ export declare const OPENAI_GPT_56_LONG_CONTEXT_COSTS: {
65
+ readonly luna: {
66
+ readonly inputThreshold: 272000;
67
+ readonly input: 0.4;
68
+ readonly output: 1.8;
69
+ readonly cacheRead: 0.04;
70
+ readonly cacheWrite: 0.5;
71
+ };
72
+ readonly sol: {
73
+ readonly inputThreshold: 272000;
74
+ readonly input: 10;
75
+ readonly output: 45;
76
+ readonly cacheRead: 1;
77
+ readonly cacheWrite: 12.5;
78
+ };
79
+ readonly terra: {
80
+ readonly inputThreshold: 272000;
81
+ readonly input: 4;
82
+ readonly output: 18;
83
+ readonly cacheRead: 0.4;
84
+ readonly cacheWrite: 5;
85
+ };
86
+ };
64
87
  export interface OpenAIModelManagerConfig {
65
88
  apiKey?: string;
66
89
  baseUrl?: string;
67
90
  fetch?: FetchImpl;
68
91
  }
69
92
  export declare function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses">;
93
+ /**
94
+ * Daybreak models are approval-gated first-party Responses models that are not
95
+ * yet present in stencil.so. Seed the documented aliases and current Cyber
96
+ * snapshot so fresh installs expose them without credentialed discovery.
97
+ */
98
+ export declare const OPENAI_DAYBREAK_CURATED_FALLBACK_MODELS: readonly ModelSpec<"openai-responses">[];
70
99
  /**
71
100
  * Re-derive the generated pro-reasoning aliases (`gpt-5.6-*-pro`) for the
72
101
  * first-party `openai` gpt-5.6 rows. Each alias inherits the base row's
@@ -122,7 +122,7 @@ export interface Usage {
122
122
  };
123
123
  }
124
124
  export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "kimi" | "qwen" | "qwen-chat-template";
125
- export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
125
+ export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "none-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
126
126
  export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
127
127
  /**
128
128
  * Compatibility settings for openai-completions API.
@@ -152,13 +152,6 @@ export interface OpenAICompat {
152
152
  reasoningEffortMap?: Partial<Record<Effort, string>>;
153
153
  /** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
154
154
  supportsUsageInStreaming?: boolean;
155
- /**
156
- * Enable the Gemini thinking-loop guard (pi-ai stream layer) for this model.
157
- * Defaults to true when the model id classifies as the gemini family. Set
158
- * explicitly to cover an opaque OpenAI-compat proxy alias (e.g. `my-model`)
159
- * that routes to Gemini, or to false to opt a gemini-family id out.
160
- */
161
- enableGeminiThinkingLoopGuard?: boolean;
162
155
  /** Which field to use for max tokens. Default: auto-detected from URL. */
163
156
  maxTokensField?: "max_completion_tokens" | "max_tokens";
164
157
  /** Whether tool results require the `name` field. Default: auto-detected from URL. */
@@ -582,8 +575,6 @@ export interface ResolvedOpenAISharedCompat {
582
575
  isOpenRouterHost: boolean;
583
576
  /** Whether this endpoint needs a max-token field even when caller did not set one. */
584
577
  alwaysSendMaxTokens: boolean;
585
- /** See {@link OpenAICompat.enableGeminiThinkingLoopGuard}. Set by the builder from the family classifier. */
586
- enableGeminiThinkingLoopGuard?: boolean;
587
578
  openRouterRouting?: OpenAICompat["openRouterRouting"];
588
579
  /** Provider-specific wire model-id transform applied to the base id. */
589
580
  wireModelIdMode: "raw" | "firepass" | "fireworks" | "openrouter";
@@ -596,7 +587,7 @@ export interface ResolvedOpenAISharedCompat {
596
587
  * `buildModel`; request handlers read fields and never detect, resolve, or
597
588
  * allocate.
598
589
  */
599
- export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "kimiApiFormat" | "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" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
590
+ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "kimiApiFormat" | "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" | "supportsPromptCacheBreakpoints" | "promptCacheBreakpointTtl" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamFirstEventTimeoutMs" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "whenThinking">> & {
600
591
  vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
601
592
  extraBody?: OpenAICompat["extraBody"];
602
593
  cacheControlFormat?: OpenAICompat["cacheControlFormat"];
@@ -681,6 +672,25 @@ export interface RemoteCompactionConfig<TApi extends Api = Api> {
681
672
  /** Model id sent to the compaction endpoint when it differs from the active model id. */
682
673
  model?: string;
683
674
  }
675
+ /** Per-million-token rates for one model pricing tier. */
676
+ export interface TokenCost {
677
+ input: number;
678
+ output: number;
679
+ cacheRead: number;
680
+ cacheWrite: number;
681
+ }
682
+ /**
683
+ * Rates applied to the full request when its prompt exceeds `inputThreshold`.
684
+ * Prompt input is the sum of uncached, cached-read, cache-write, and
685
+ * provider-orchestration input tokens.
686
+ */
687
+ export interface LongContextTokenCost extends TokenCost {
688
+ inputThreshold: number;
689
+ }
690
+ /** Base token rates plus an optional long-context tier. */
691
+ export interface ModelCost extends TokenCost {
692
+ longContext?: LongContextTokenCost;
693
+ }
684
694
  export interface Model<TApi extends Api = Api> {
685
695
  id: string;
686
696
  /**
@@ -725,12 +735,7 @@ export interface Model<TApi extends Api = Api> {
725
735
  gitlabDuoWorkflowRootNamespaceId?: string;
726
736
  /** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
727
737
  cursorMaxMode?: boolean;
728
- cost: {
729
- input: number;
730
- output: number;
731
- cacheRead: number;
732
- cacheWrite: number;
733
- };
738
+ cost: ModelCost;
734
739
  /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
735
740
  premiumMultiplier?: number;
736
741
  contextWindow: number | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "17.2.15",
4
+ "version": "17.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",
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@bufbuild/protobuf": "^2.12.1",
38
- "@oh-my-pi/omptype": "17.2.15",
39
- "@oh-my-pi/pi-utils": "17.2.15"
38
+ "@oh-my-pi/omptype": "17.3.0",
39
+ "@oh-my-pi/pi-utils": "17.3.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "17.2.15",
42
+ "@oh-my-pi/pi-ai": "17.3.0",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
package/src/build.ts CHANGED
@@ -14,12 +14,11 @@ import { buildAnthropicCompat } from "./compat/anthropic";
14
14
  import { buildBedrockCompat } from "./compat/bedrock";
15
15
  import { buildDevinCompat } from "./compat/devin";
16
16
  import { buildOpenAICompat, buildOpenAIResponsesCompat, buildOpenRouterCompat } from "./compat/openai";
17
+ import { bareModelId, parseOpenAIModel, semverGte } from "./identity/classify";
17
18
  import { resolveModelThinking } from "./model-thinking";
18
19
  import type { Api, CompatOf, Model, ModelSpec } from "./types";
19
20
  import { cleanModelName } from "./utils";
20
21
 
21
- const OPENAI_GA_COMPUTER_MODEL_RE = /^gpt-5\.(?:[4-9]|[1-9]\d)(?:[.-]|$)/i;
22
-
23
22
  function isDirectOpenAIResponsesEndpoint(spec: ModelSpec<Api>): boolean {
24
23
  if (spec.api === "openai-responses") {
25
24
  if (spec.provider !== "openai") return false;
@@ -55,7 +54,8 @@ function explicitComputerUseConfig(spec: ModelSpec<Api>): boolean | undefined {
55
54
  function supportsOpenAIGAComputerUse(spec: ModelSpec<Api>, explicitSupport: boolean | undefined): boolean {
56
55
  if (explicitSupport !== undefined) return explicitSupport;
57
56
  if (!isDirectOpenAIResponsesEndpoint(spec)) return false;
58
- return OPENAI_GA_COMPUTER_MODEL_RE.test(spec.requestModelId ?? spec.id);
57
+ const parsed = parseOpenAIModel(bareModelId(spec.requestModelId ?? spec.id));
58
+ return parsed !== null && semverGte(parsed.version, "5.4");
59
59
  }
60
60
 
61
61
  export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi> {
@@ -22,7 +22,6 @@ import {
22
22
  isMimoModelIdOrName,
23
23
  isOpenAISamplingRestrictedModelId,
24
24
  isQwenModelId,
25
- modelFamilyToken,
26
25
  } from "../identity/family";
27
26
  import type {
28
27
  ModelSpec,
@@ -474,10 +473,6 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
474
473
  supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
475
474
  reasoningEffortMap: {},
476
475
  supportsUsageInStreaming: !isCerebras,
477
- // pi-ai's thinking-loop guard is gemini-only; default the flag from the
478
- // family classifier so OpenAI-compat proxies serving Gemini are covered.
479
- // An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.
480
- enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === "gemini",
481
476
  // Kimi (including via OpenRouter and Fireworks router-form IDs such as
482
477
  // `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on
483
478
  // max_tokens, not actual output. The official Kimi K2 model guidance
@@ -749,7 +744,6 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
749
744
  // lands on Moonshot's MFJS validator.
750
745
  toolSchemaFlavor: isKimiModel ? "moonshot-mfjs" : undefined,
751
746
  alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
752
- enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
753
747
  supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
754
748
  stripDeepseekSpecialTokens:
755
749
  Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
@@ -4,6 +4,18 @@ import { discoveryFetch } from "../utils";
4
4
 
5
5
  const MODELS_PATH = "/models";
6
6
 
7
+ /**
8
+ * Default hard deadline applied to an OpenAI-compatible `/models` probe when
9
+ * the caller supplies neither an `AbortSignal` nor an explicit `timeoutMs`.
10
+ *
11
+ * Built-in provider model managers (openrouter, xAI, DeepSeek, …) call
12
+ * {@link fetchOpenAICompatibleModels} with no timeout, so without this bound a
13
+ * stalled endpoint left the request pending forever and blocked startup's
14
+ * awaited `resolveModelDiscoveryFallback` discovery pass indefinitely
15
+ * (issue #8315). 10s matches the coding-agent's remote-discovery budget.
16
+ */
17
+ export const DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS = 10_000;
18
+
7
19
  /**
8
20
  * Uses a cancellable timer rather than the native abort-timeout helper so
9
21
  * successful fast discovery requests do not leave armed timeout signals for
@@ -96,7 +108,11 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
96
108
  headers?: Record<string, string>;
97
109
  /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
98
110
  signal?: AbortSignal;
99
- /** Optional cancellable request timeout used when `signal` is omitted. */
111
+ /**
112
+ * Optional cancellable request timeout used when `signal` is omitted.
113
+ * Defaults to {@link DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS} so a
114
+ * stalled endpoint can never hang discovery indefinitely.
115
+ */
100
116
  timeoutMs?: number;
101
117
  /** Optional fetch implementation override for testing/custom runtimes. */
102
118
  fetch?: FetchImpl;
@@ -164,9 +180,10 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
164
180
  const payload =
165
181
  options.signal !== undefined
166
182
  ? await fetchPayload(options.signal)
167
- : options.timeoutMs !== undefined
168
- ? await withOpenAICompatibleDiscoveryTimeout(options.timeoutMs, fetchPayload)
169
- : await fetchPayload();
183
+ : await withOpenAICompatibleDiscoveryTimeout(
184
+ options.timeoutMs ?? DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
185
+ fetchPayload,
186
+ );
170
187
  if (payload === null) {
171
188
  return null;
172
189
  }
@@ -122,16 +122,31 @@ export const parseAnthropicModel = parser((modelId): AnthropicModel | null => {
122
122
  return { family: "anthropic", kind: kind as AnthropicKind, version };
123
123
  });
124
124
 
125
+ /**
126
+ * Rolling OpenAI aliases inherit wire capabilities from their current default
127
+ * snapshots. Keep this map aligned with the model docs when an alias advances.
128
+ */
129
+ const OPENAI_ALIAS_VERSIONS: Readonly<Record<string, string>> = {
130
+ "daybreak-blue-latest": "5.6",
131
+ "gpt-daybreak-blue-latest": "5.6",
132
+ "daybreak-red-latest": "5.6",
133
+ "gpt-daybreak-red-latest": "5.6",
134
+ };
135
+
125
136
  export const parseOpenAIModel = parser((modelId): OpenAIModel | null => {
126
- const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?\b/.exec(modelId);
127
- if (!match) {
137
+ const aliasVersion = OPENAI_ALIAS_VERSIONS[modelId];
138
+ const match = aliasVersion
139
+ ? null
140
+ : /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?\b/.exec(modelId);
141
+ const versionInput = aliasVersion ?? match?.[1];
142
+ if (!versionInput) {
128
143
  return null;
129
144
  }
130
- const version = parseSemVer(match[1]);
145
+ const version = parseSemVer(versionInput);
131
146
  if (!version) {
132
147
  return null;
133
148
  }
134
- return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version };
149
+ return { family: "openai", variant: (match?.[2] as OpenAIVariant | undefined) ?? "base", version };
135
150
  });
136
151
 
137
152
  /**
@@ -85,9 +85,11 @@ export const isDeepseekModelIdOrName = memo((value: string): boolean => {
85
85
 
86
86
  /**
87
87
  * DeepSeek V4 Flash SKU in any host/namespace form (`deepseek-v4-flash`, dated
88
- * `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Flash is the only
89
- * V4 model whose `reasoning_effort` accepts the `low` tier; V4 Pro tops out at
90
- * `high`/`max`. See https://api-docs.deepseek.com/api/create-chat-completion.
88
+ * `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Both V4 SKUs
89
+ * (Flash and Pro) accept the `low` reasoning_effort tier; this predicate keeps
90
+ * Flash distinguishable from Pro where a host quirk splits them (e.g.
91
+ * OpenRouter exposes `low` on Flash but only `high` on non-Flash V4).
92
+ * See https://api-docs.deepseek.com/api/create-chat-completion.
91
93
  */
92
94
  export const isDeepseekV4FlashModelId = memo((modelId: string): boolean => {
93
95
  return bareModelId(modelId).toLowerCase().includes("deepseek-v4-flash");
@@ -98,6 +100,16 @@ export const isMimoModelIdOrName = memo((value: string): boolean => {
98
100
  return value.toLowerCase().includes("mimo");
99
101
  });
100
102
 
103
+ /** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
104
+ export const isGeminiModelId = memo((modelId: string): boolean => {
105
+ return /(^|\/)gemini[-.]?/i.test(modelId);
106
+ });
107
+
108
+ /** Grok family ids across namespace and delimiter forms (`grok-*`, `cursor-grok-*`, `xai/grok-*`). */
109
+ export const isGrokModelId = memo((modelId: string): boolean => {
110
+ return /(?:^|[./_-])grok(?:[-.]|$)/i.test(modelId);
111
+ });
112
+
101
113
  const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3", "grok-4.5"] as const;
102
114
 
103
115
  /**
@@ -259,12 +271,14 @@ export const modelFamilyToken = memo((modelId: string): string => {
259
271
  const parsed = parseKnownModel(modelId);
260
272
  if (parsed.family !== "unknown") return parsed.family;
261
273
  if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
274
+ if (isGeminiModelId(modelId)) return "gemini";
275
+ if (isGrokModelId(modelId)) return "grok";
276
+ if (isDeepseekModelIdOrName(modelId)) return "deepseek";
262
277
  if (isOpenAIModelId(modelId)) return "openai";
263
278
  if (isKimiModelId(modelId)) return "kimi";
264
279
  if (isQwenModelId(modelId)) return "qwen";
265
280
  if (isMinimaxM2FamilyModelId(modelId) || isMinimaxM3FamilyModelId(modelId)) return "minimax";
266
281
  if (isOpenAIGptOssModelId(modelId)) return "gpt-oss";
267
- if (isDeepseekModelIdOrName(modelId)) return "deepseek";
268
282
  if (isMimoModelIdOrName(modelId)) return "mimo";
269
283
  if (isGemmaModelId(modelId)) return "gemma";
270
284
  if (parseGlmModel(bareModelId(modelId))) return "glm";
@@ -1,7 +1,7 @@
1
1
  import { buildModel } from "./build";
2
2
  import { readModelCache, writeModelCache } from "./model-cache";
3
3
  import { type GeneratedProvider, getBundledModels } from "./models";
4
- import type { Api, Model, ModelSpec, Provider } from "./types";
4
+ import type { Api, Model, ModelCost, ModelSpec, Provider, TokenCost } from "./types";
5
5
  import { isRecord } from "./utils";
6
6
  import { collapseBuiltModelVariants } from "./variant-collapse";
7
7
 
@@ -510,6 +510,7 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
510
510
  const reasoning = dynamicReasoningAuthoritative
511
511
  ? dynamicModel.reasoning
512
512
  : existingModel.reasoning || dynamicModel.reasoning;
513
+ const longContextCost = dynamicModel.cost.longContext ?? existingModel.cost.longContext;
513
514
  // Re-build from spec stage: sparse compat comes from `compatConfig` (the
514
515
  // verbatim override vocabulary), never the resolved `compat` record.
515
516
  return buildModel({
@@ -523,6 +524,7 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
523
524
  output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output),
524
525
  cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
525
526
  cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
527
+ ...(longContextCost ? { longContext: longContextCost } : {}),
526
528
  },
527
529
  contextWindow: preferDiscoveryLimit(dynamicModel.contextWindow, existingModel.contextWindow),
528
530
  maxTokens: preferDiscoveryLimit(dynamicModel.maxTokens, existingModel.maxTokens),
@@ -640,7 +642,7 @@ function isModelInputArray(value: unknown): value is ("text" | "image")[] {
640
642
  return true;
641
643
  }
642
644
 
643
- function isModelCost(value: unknown): value is Model<Api>["cost"] {
645
+ function isTokenCost(value: unknown): value is TokenCost {
644
646
  if (!isRecord(value)) {
645
647
  return false;
646
648
  }
@@ -670,3 +672,12 @@ function isModelCost(value: unknown): value is Model<Api>["cost"] {
670
672
  }
671
673
  return true;
672
674
  }
675
+
676
+ function isModelCost(value: unknown): value is ModelCost {
677
+ if (!isTokenCost(value)) return false;
678
+ const longContext = (value as TokenCost & { longContext?: unknown }).longContext;
679
+ if (longContext === undefined) return true;
680
+ if (!isTokenCost(longContext) || !isRecord(longContext)) return false;
681
+ const threshold = longContext.inputThreshold;
682
+ return typeof threshold === "number" && threshold > 0 && threshold < Infinity;
683
+ }
@@ -63,9 +63,9 @@ const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, E
63
63
  const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
64
64
  const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
65
65
  const LOW_MEDIUM_HIGH_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
66
- /** Wire-exact `low`/`high`/`max` scale used by Kimi K3 and DeepSeek V4 Flash (direct API and aggregators). */
66
+ /** Wire-exact `low`/`high`/`max` scale used by Kimi K3 and DeepSeek V4 (Flash and Pro, direct API and aggregators). */
67
67
  const LOW_HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
68
- /** Wire-exact two-tier scale (`high`/`max`): GLM-5.2 on Z.ai/Umans/Ollama Cloud/Baseten, Sakana Fugu, DeepSeek V4 Pro. */
68
+ /** Wire-exact two-tier scale (`high`/`max`): GLM-5.2 on Z.ai/Umans/Ollama Cloud/Baseten, Sakana Fugu, older DeepSeek reasoners (V3.x/R1). */
69
69
  const HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.Max];
70
70
  /** OpenRouter's DeepSeek route accepts only `high`. */
71
71
  const HIGH_ONLY_REASONING_EFFORTS: readonly Effort[] = [Effort.High];
@@ -366,14 +366,22 @@ function getModelDefinedEfforts<TApi extends Api>(
366
366
  if (spec.provider === "ollama") {
367
367
  return OLLAMA_REASONING_EFFORTS;
368
368
  }
369
- if (isOpenAICompatReasoningApi(spec.api) && isDeepseekReasoningModel(spec)) {
370
- // DeepSeek V4 Flash accepts the wire-exact low/high/max ladder on every
371
- // host — the direct API and aggregators alike (medium/xhigh map to
372
- // high). V4 Pro and the older reasoners top out at high/max, and
373
- // OpenRouter's non-flash DeepSeek route exposes only high.
369
+ if (
370
+ (isOpenAICompatReasoningApi(spec.api) || (spec.api === "ollama-chat" && spec.provider === "ollama-cloud")) &&
371
+ isDeepseekReasoningModel(spec)
372
+ ) {
373
+ // DeepSeek V4 (Flash and Pro) accepts the wire-exact low/high/max ladder
374
+ // on every first-party/aggregator host — the direct API, aggregators, and
375
+ // Ollama Cloud alike (medium/xhigh fold into high, max is a real wire
376
+ // tier). See https://api-docs.deepseek.com/api/create-chat-completion.
377
+ // OpenRouter's non-Flash V4 route still exposes only high; the older
378
+ // reasoners (V3.x, R1, deepseek-reasoner) top out at high/max.
374
379
  if (isDeepseekV4FlashModelId(spec.id)) {
375
380
  return LOW_HIGH_MAX_REASONING_EFFORTS;
376
381
  }
382
+ if (bareModelId(spec.id).toLowerCase().includes("deepseek-v4")) {
383
+ return isOpenRouterThinkingFormat(compat) ? HIGH_ONLY_REASONING_EFFORTS : LOW_HIGH_MAX_REASONING_EFFORTS;
384
+ }
377
385
  return isOpenRouterThinkingFormat(compat) ? HIGH_ONLY_REASONING_EFFORTS : HIGH_MAX_REASONING_EFFORTS;
378
386
  }
379
387
  if (spec.provider === "baseten" && isOpenAIGptOssModelId(spec.id)) {