@oh-my-pi/pi-catalog 16.2.8 → 16.2.11

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,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.11] - 2026-07-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed a potential memory leak caused by dangling timeout timers during model discovery in OpenAI-compatible, vLLM, LiteLLM, and LM Studio catalogs.
10
+ - Widened stream watchdogs for local OpenAI-compatible backends (including llama.cpp, LM Studio, vLLM, and Ollama) to prevent premature timeouts during cold model loads.
11
+
12
+ ## [16.2.10] - 2026-06-30
13
+
14
+ ### Added
15
+
16
+ - Added Claude Sonnet 3.7, Claude Opus 3, and Claude Sonnet 3 model entries to the Anthropic catalog
17
+ - Added Anthropic Claude Sonnet 5 model entry to the Kilo provider catalog
18
+ - Added first-party catalog discovery support for the Anthropic provider
19
+ - Added Gemini 3.1 Flash Lite Image model entry to the Kilo provider catalog
20
+ - Added Anthropic Claude Sonnet 5 model variants with low, medium, high, xhigh, and max thinking efforts to the Devin provider catalog
21
+ - Added Claude Sonnet 5 model entry to the Anthropic curated catalog.
22
+
23
+ ### Changed
24
+
25
+ - Updated the base API URL for the Claude Sonnet 5 model in the Anthropic catalog
26
+ - Updated pricing metrics for DeepSeek R1 and DeepSeek V3 model entries to reflect new rates
27
+
28
+ ## [16.2.9] - 2026-06-30
29
+
30
+ ### Added
31
+
32
+ - Added full capability support for Claude Sonnet 5, aligning it with Claude Opus 4.8 and Fable 5. This includes adaptive thinking display, mid-conversation system messages, sampling parameter and thinking omission API restrictions, and 5-tier adaptive reasoning effort mapping (including xhigh and max levels) across direct APIs, OpenRouter, and Bedrock Converse.
33
+
34
+ ### Changed
35
+
36
+ - Updated input and output costs for models in the catalog.
37
+
5
38
  ## [16.2.7] - 2026-06-30
6
39
 
7
40
  ### Fixed
@@ -48,8 +48,10 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
48
48
  apiKey?: string;
49
49
  /** Additional request headers. */
50
50
  headers?: Record<string, string>;
51
- /** Optional AbortSignal for request cancellation. */
51
+ /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
52
52
  signal?: AbortSignal;
53
+ /** Optional cancellable request timeout used when `signal` is omitted. */
54
+ timeoutMs?: number;
53
55
  /** Optional fetch implementation override for testing/custom runtimes. */
54
56
  fetch?: FetchImpl;
55
57
  /**
@@ -56,6 +56,13 @@ export declare const parseOpenAIModel: (modelId: string) => OpenAIModel | null;
56
56
  */
57
57
  export declare const parseGlmModel: (modelId: string) => GlmModel | null;
58
58
  export declare function isFableOrMythos(kind: AnthropicKind): boolean;
59
+ /**
60
+ * Returns true if the parsed Anthropic model is part of the adaptive-thinking
61
+ * Claude generation at or above a specific capability threshold.
62
+ * - Opus has a configurable minimum version floor (e.g. "4.6", "4.7", "4.8").
63
+ * - Sonnet, Fable, and Mythos all require version 5 or higher.
64
+ */
65
+ export declare function isAnthropicAdaptiveGenAtLeast(parsed: AnthropicModel, opusMin: "4.6" | "4.7" | "4.8"): boolean;
59
66
  export declare function parseSemVer(version: string): SemVer | null;
60
67
  export declare function semverGte(left: SemVer | string, right: SemVer | string): boolean;
61
68
  export declare function semverEqual(left: SemVer | string, right: SemVer | string): boolean;
@@ -85,15 +85,16 @@ export declare const isGlmVisionModelId: (modelId: string) => boolean;
85
85
  */
86
86
  export declare const modelFamilyToken: (modelId: string) => string;
87
87
  /**
88
- * Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
89
- * the Claude Fable/Mythos 5 generation. Older adaptive-thinking models
90
- * (Opus 4.6, Sonnet 4.6+) reject the field. Classifier-based, so dotted and
91
- * dashed version forms both match while bare dated ids
88
+ * Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
89
+ * Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
90
+ * models (Opus 4.6, Sonnet 4.6) reject the field. Classifier-based, so dotted
91
+ * and dashed version forms both match while bare dated ids
92
92
  * (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
93
93
  */
94
94
  export declare const supportsAdaptiveThinkingDisplay: (modelId: string) => boolean;
95
95
  /**
96
- * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
96
+ * Returns true for Anthropic models with Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+
97
+ * API restrictions:
97
98
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
98
99
  * - Thinking content is omitted by default (needs display: "summarized")
99
100
  */
@@ -101,8 +102,8 @@ export declare const hasOpus47ApiRestrictions: (modelId: string) => boolean;
101
102
  /**
102
103
  * Mid-conversation `role: "system"` messages (system instructions appended at
103
104
  * non-first positions in the `messages` array) are supported starting with
104
- * Claude Opus 4.8 and the Claude Fable/Mythos 5 generation. Earlier Claude
105
- * models reject the role.
105
+ * Claude Opus 4.8+, Sonnet 5+, and the Claude Fable/Mythos 5 generation.
106
+ * Earlier Claude models reject the role.
106
107
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
107
108
  */
108
109
  export declare const supportsMidConversationSystemMessages: (modelId: string) => boolean;
@@ -29,7 +29,11 @@ export declare const CATALOG_PROVIDERS: readonly [{
29
29
  }, {
30
30
  readonly id: "anthropic";
31
31
  readonly defaultModel: "claude-opus-4-8";
32
+ readonly envVars: readonly ["ANTHROPIC_API_KEY"];
32
33
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("..").ModelManagerOptions<"anthropic-messages", unknown>;
34
+ readonly catalogDiscovery: {
35
+ readonly label: "Anthropic";
36
+ };
33
37
  }, {
34
38
  readonly id: "azure";
35
39
  readonly defaultModel: "gpt-5.5";
@@ -29,8 +29,8 @@ export interface ModelsDevModel {
29
29
  * first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
30
30
  * Seeded into model generation so the bundled catalog is never gated on
31
31
  * models.dev's update cadence; deduped behind upstream catalog / models.dev
32
- * entries once those appear. Token limits and pricing are pinned
33
- * authoritatively in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked
32
+ * entries once those appear. Token limits and pricing are pinned either directly or
33
+ * in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked
34
34
  * by the generator's policy pass (scripts/generated-policies.ts).
35
35
  */
36
36
  export declare const ANTHROPIC_CURATED_FALLBACK_MODELS: readonly ModelSpec<"anthropic-messages">[];
@@ -371,6 +371,7 @@ export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
371
371
  headers?: Record<string, string>;
372
372
  fetch?: FetchImpl;
373
373
  signal?: AbortSignal;
374
+ timeoutMs?: number;
374
375
  referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
375
376
  }
376
377
  export declare const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128000;
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.8",
4
+ "version": "16.2.11",
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.8",
37
+ "@oh-my-pi/pi-utils": "16.2.11",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.2.8",
42
+ "@oh-my-pi/pi-ai": "16.2.11",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -57,6 +57,8 @@ function matchesKimiK27CodeFamily(spec: ModelSpec<"openai-completions">): boolea
57
57
  const XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;
58
58
  /** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */
59
59
  const ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
60
+ /** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */
61
+ const LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;
60
62
  const MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;
61
63
  const DSML_HEALING_PROVIDERS = new Set([
62
64
  "ollama",
@@ -294,6 +296,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
294
296
  isMoonshotNative ||
295
297
  isOpenCodeHost;
296
298
  const isOpenCodeProvider = provider === "opencode-go" || provider === "opencode-zen";
299
+ const isLocalOpenAICompatBackend =
300
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
301
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));
297
302
 
298
303
  const useMaxTokens =
299
304
  isMistral ||
@@ -348,9 +353,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
348
353
  isCopilotHost ||
349
354
  isZenmuxHost);
350
355
 
351
- // Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, and direct
352
- // DeepSeek reasoning models can idle for minutes while reasoning; widen the
353
- // idle timeout so warm-ups stop aborting and retrying.
356
+ // Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct
357
+ // DeepSeek reasoning models, and local OpenAI-compatible backends can idle
358
+ // for minutes while reasoning or cold-loading weights; widen the idle
359
+ // timeout so warm-ups stop aborting and retrying.
354
360
  const streamIdleTimeoutMs =
355
361
  GLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)
356
362
  ? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS
@@ -362,7 +368,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
362
368
  ? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS
363
369
  : spec.reasoning && isDirectDeepseekApi
364
370
  ? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
365
- : undefined;
371
+ : isLocalOpenAICompatBackend
372
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
373
+ : undefined;
366
374
 
367
375
  // Fireworks "Fast" variants (`<id>-fast`) are served from the router
368
376
  // namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather
@@ -482,9 +490,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
482
490
  // when a thinking block actually exists on the turn
483
491
  // (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on
484
492
  // pure-text histories.
485
- replayReasoningContent:
486
- !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
487
- (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl)),
493
+ replayReasoningContent: isLocalOpenAICompatBackend,
488
494
  // `preserve_thinking: true` makes the Qwen3.6+ chat template render
489
495
  // `<think>...</think>` for older assistant turns too, instead of
490
496
  // stripping it the moment a new user message moves them past
@@ -496,9 +502,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
496
502
  // with `replayReasoningContent` above). Non-Qwen templates ignore the
497
503
  // parameter, so the flag stays a no-op outside the Qwen path.
498
504
  qwenPreserveThinking:
499
- (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") &&
500
- !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
501
- (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl)),
505
+ (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && isLocalOpenAICompatBackend,
502
506
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
503
507
  cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
504
508
  openRouterRouting: undefined,
@@ -584,6 +588,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
584
588
  const isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;
585
589
  const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
586
590
  const reasoningCapable = Boolean(spec.reasoning);
591
+ const isLocalOpenAICompatBackend =
592
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&
593
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));
587
594
 
588
595
  const compat: ResolvedOpenAIResponsesCompat = {
589
596
  supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
@@ -645,7 +652,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
645
652
  emptyLengthFinishIsContextError: spec.provider === "ollama",
646
653
  usesOpenAIToolCallIdLimit: spec.provider === "openai",
647
654
  promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
648
- streamIdleTimeoutMs: spec.compat?.streamIdleTimeoutMs,
655
+ streamIdleTimeoutMs: isLocalOpenAICompatBackend
656
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
657
+ : spec.compat?.streamIdleTimeoutMs,
649
658
  };
650
659
  applyCompatOverrides(compat, spec.compat);
651
660
  if (spec.compat?.reasoningDisableMode === undefined) {
@@ -3,6 +3,27 @@ import type { Api, FetchImpl, ModelSpec, Provider } from "../types";
3
3
 
4
4
  const MODELS_PATH = "/models";
5
5
 
6
+ /**
7
+ * Uses a cancellable timer rather than the native abort-timeout helper so
8
+ * successful fast discovery requests do not leave armed timeout signals for
9
+ * concurrent GC to trip over later.
10
+ */
11
+ async function withOpenAICompatibleDiscoveryTimeout<T>(
12
+ timeoutMs: number,
13
+ run: (signal: AbortSignal) => Promise<T>,
14
+ ): Promise<T> {
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(
17
+ () => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
18
+ timeoutMs,
19
+ );
20
+ try {
21
+ return await run(controller.signal);
22
+ } finally {
23
+ clearTimeout(timer);
24
+ }
25
+ }
26
+
6
27
  /**
7
28
  * Minimal OpenAI-style model entry shape consumed by discovery.
8
29
  *
@@ -72,8 +93,10 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
72
93
  apiKey?: string;
73
94
  /** Additional request headers. */
74
95
  headers?: Record<string, string>;
75
- /** Optional AbortSignal for request cancellation. */
96
+ /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
76
97
  signal?: AbortSignal;
98
+ /** Optional cancellable request timeout used when `signal` is omitted. */
99
+ timeoutMs?: number;
77
100
  /** Optional fetch implementation override for testing/custom runtimes. */
78
101
  fetch?: FetchImpl;
79
102
  /**
@@ -115,25 +138,35 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
115
138
  }
116
139
 
117
140
  const fetchImpl = options.fetch ?? globalThis.fetch;
118
- let response: Response;
119
- try {
120
- response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
121
- method: "GET",
122
- headers: requestHeaders,
123
- signal: options.signal,
124
- });
125
- } catch {
126
- return null;
127
- }
141
+ const fetchPayload = async (signal?: AbortSignal): Promise<unknown | null> => {
142
+ let response: Response;
143
+ try {
144
+ response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
145
+ method: "GET",
146
+ headers: requestHeaders,
147
+ signal,
148
+ });
149
+ } catch {
150
+ return null;
151
+ }
128
152
 
129
- if (!response.ok) {
130
- return null;
131
- }
153
+ if (!response.ok) {
154
+ return null;
155
+ }
132
156
 
133
- let payload: unknown;
134
- try {
135
- payload = await response.json();
136
- } catch {
157
+ try {
158
+ return await response.json();
159
+ } catch {
160
+ return null;
161
+ }
162
+ };
163
+ const payload =
164
+ options.signal !== undefined
165
+ ? await fetchPayload(options.signal)
166
+ : options.timeoutMs !== undefined
167
+ ? await withOpenAICompatibleDiscoveryTimeout(options.timeoutMs, fetchPayload)
168
+ : await fetchPayload();
169
+ if (payload === null) {
137
170
  return null;
138
171
  }
139
172
 
@@ -158,6 +158,20 @@ export function isFableOrMythos(kind: AnthropicKind): boolean {
158
158
  return kind === "fable" || kind === "mythos";
159
159
  }
160
160
 
161
+ /**
162
+ * Returns true if the parsed Anthropic model is part of the adaptive-thinking
163
+ * Claude generation at or above a specific capability threshold.
164
+ * - Opus has a configurable minimum version floor (e.g. "4.6", "4.7", "4.8").
165
+ * - Sonnet, Fable, and Mythos all require version 5 or higher.
166
+ */
167
+ export function isAnthropicAdaptiveGenAtLeast(parsed: AnthropicModel, opusMin: "4.6" | "4.7" | "4.8"): boolean {
168
+ if (parsed.kind === "opus") {
169
+ return semverGte(parsed.version, opusMin);
170
+ }
171
+ // Sonnet 5+, Fable 5+, Mythos 5+, and any future gen-5+ models
172
+ return semverGte(parsed.version, "5");
173
+ }
174
+
161
175
  function createSemVer(major: number, minor: number, patch = 0): SemVer {
162
176
  return { major, minor, patch };
163
177
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import {
11
11
  bareModelId,
12
+ isAnthropicAdaptiveGenAtLeast,
12
13
  isFableOrMythos,
13
14
  parseAnthropicModel,
14
15
  parseGlmModel,
@@ -201,41 +202,38 @@ export const modelFamilyToken = memo((modelId: string): string => {
201
202
  });
202
203
 
203
204
  /**
204
- * Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
205
- * the Claude Fable/Mythos 5 generation. Older adaptive-thinking models
206
- * (Opus 4.6, Sonnet 4.6+) reject the field. Classifier-based, so dotted and
207
- * dashed version forms both match while bare dated ids
205
+ * Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
206
+ * Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
207
+ * models (Opus 4.6, Sonnet 4.6) reject the field. Classifier-based, so dotted
208
+ * and dashed version forms both match while bare dated ids
208
209
  * (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
209
210
  */
210
211
  export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
211
212
  const parsed = parseAnthropicModel(bareModelId(modelId));
212
- if (!parsed) return false;
213
- if (isFableOrMythos(parsed.kind)) return semverGte(parsed.version, "5");
214
- return parsed.kind === "opus" && semverGte(parsed.version, "4.7");
213
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
215
214
  });
216
215
 
217
216
  /**
218
- * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
217
+ * Returns true for Anthropic models with Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+
218
+ * API restrictions:
219
219
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
220
220
  * - Thinking content is omitted by default (needs display: "summarized")
221
221
  */
222
222
  export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
223
223
  const parsed = parseAnthropicModel(bareModelId(modelId));
224
- if (!parsed) return false;
225
- return (parsed.kind === "opus" && semverGte(parsed.version, "4.7")) || isFableOrMythos(parsed.kind);
224
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
226
225
  });
227
226
 
228
227
  /**
229
228
  * Mid-conversation `role: "system"` messages (system instructions appended at
230
229
  * non-first positions in the `messages` array) are supported starting with
231
- * Claude Opus 4.8 and the Claude Fable/Mythos 5 generation. Earlier Claude
232
- * models reject the role.
230
+ * Claude Opus 4.8+, Sonnet 5+, and the Claude Fable/Mythos 5 generation.
231
+ * Earlier Claude models reject the role.
233
232
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
234
233
  */
235
234
  export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
236
235
  const parsed = parseAnthropicModel(bareModelId(modelId));
237
- if (!parsed) return false;
238
- return (parsed.kind === "opus" && semverGte(parsed.version, "4.8")) || isFableOrMythos(parsed.kind);
236
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.8");
239
237
  });
240
238
 
241
239
  export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
@@ -12,7 +12,7 @@ import {
12
12
  type AnthropicModel,
13
13
  bareModelId,
14
14
  type GeminiModel,
15
- isFableOrMythos,
15
+ isAnthropicAdaptiveGenAtLeast,
16
16
  type OpenAIModel,
17
17
  type ParsedModel,
18
18
  parseAnthropicModel,
@@ -441,12 +441,11 @@ function isDeepseekReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): bool
441
441
  function getOpenRouterAnthropicReasoningEffortMap(modelId: string): EffortMap | undefined {
442
442
  const parsed = parseAnthropicModel(bareModelId(modelId));
443
443
  if (!parsed) return undefined;
444
- // Adaptive efforts on OpenRouter's completions front: Fable/Mythos and
445
- // Opus 4.6+ only — Sonnet stays on the plain effort vocabulary there.
446
- const isOpusAdaptive = parsed.kind === "opus" && semverGte(parsed.version, "4.6");
447
- if (!isFableOrMythos(parsed.kind) && !isOpusAdaptive) return undefined;
444
+ // Adaptive efforts on OpenRouter's completions front: Fable/Mythos, Sonnet 5+,
445
+ // and Opus 4.6+ only — older Sonnet versions stay on the plain effort vocabulary there.
446
+ if (!isAnthropicAdaptiveGenAtLeast(parsed, "4.6")) return undefined;
448
447
 
449
- const hasRealXHigh = isFableOrMythos(parsed.kind) || semverGte(parsed.version, "4.7");
448
+ const hasRealXHigh = isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
450
449
  return hasRealXHigh ? ANTHROPIC_ADAPTIVE_EFFORT_MAP_5_TIER : ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER;
451
450
  }
452
451
 
@@ -521,7 +520,7 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
521
520
  (spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") &&
522
521
  semverGte(parsedModel.version, "4.6")
523
522
  ) {
524
- return parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind)
523
+ return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6")
525
524
  ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH
526
525
  : DEFAULT_REASONING_EFFORTS;
527
526
  }
@@ -601,10 +600,7 @@ function inferThinkingControlMode<TApi extends Api>(
601
600
 
602
601
  case "bedrock-converse-stream":
603
602
  if (parsedModel.family === "anthropic") {
604
- if (
605
- semverGte(parsedModel.version, "4.6") &&
606
- (parsedModel.kind === "opus" || isFableOrMythos(parsedModel.kind))
607
- ) {
603
+ if (isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6")) {
608
604
  return "anthropic-adaptive";
609
605
  }
610
606
  // Opus 4.5 on Bedrock metadata mirrors the direct-Anthropic
@@ -627,19 +623,18 @@ function isOpenRouterAnthropicAdaptiveReasoningModel<TApi extends Api>(
627
623
  ): boolean {
628
624
  if (!isOpenAICompatReasoningApi(spec.api)) return false;
629
625
  if (!modelMatchesHost(spec, "openrouter")) return false;
630
- return isFableOrMythos(parsedModel.kind) || (parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.6"));
626
+ return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6");
631
627
  }
632
628
 
633
629
  /**
634
- * Opus 4.7+ and Fable/Mythos on the Messages API expose the full five-tier
630
+ * Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+ on the Messages API expose the full five-tier
635
631
  * adaptive scale (low/medium/high/xhigh/max). Bedrock Converse stays on the
636
632
  * four-tier scale regardless of model version.
637
633
  */
638
634
  function anthropicModelHasRealXHighEffort<TApi extends Api>(spec: ModelSpec<TApi>, parsedModel: ParsedModel): boolean {
639
635
  if (spec.api !== "anthropic-messages") return false;
640
636
  if (parsedModel.family !== "anthropic") return false;
641
- if (isFableOrMythos(parsedModel.kind)) return true;
642
- return parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.7");
637
+ return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.7");
643
638
  }
644
639
 
645
640
  // ---------------------------------------------------------------------------