@oh-my-pi/pi-catalog 16.0.6 → 16.0.8

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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.0.8] - 2026-06-18
6
+
7
+ ### Changed
8
+
9
+ - Refactored model family ID predicates and capability checkers to use a shared, uniform process-lifetime `memo` utility to eliminate caching boilerplate.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed LM Studio dynamic discovery to use native `/api/v0/models` metadata so VLM models advertise image input. ([#2945](https://github.com/can1357/oh-my-pi/issues/2945))
14
+
15
+ ## [16.0.7] - 2026-06-18
16
+
17
+ ### Fixed
18
+
19
+ - Fixed MiniMax Anthropic-compatible M2/M3 thinking metadata to expose the adaptive transport and keep M2 mandatory reasoning floored ([#2928](https://github.com/can1357/oh-my-pi/issues/2928)).
20
+
5
21
  ## [16.0.6] - 2026-06-18
6
22
 
7
23
  ### Added
@@ -41,7 +41,6 @@ export interface UnknownModel {
41
41
  id: string;
42
42
  }
43
43
  export type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
44
- /** Strip a provider namespace prefix (`openai/gpt-5.4` → `gpt-5.4`). */
45
44
  export declare function bareModelId(modelId: string): string;
46
45
  export declare function parseKnownModel(modelId: string): ParsedModel;
47
46
  export declare const parseGeminiModel: (modelId: string) => GeminiModel | null;
@@ -7,27 +7,27 @@
7
7
  * here.
8
8
  */
9
9
  /** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
10
- export declare function isKimiModelId(modelId: string): boolean;
10
+ export declare const isKimiModelId: (modelId: string) => boolean;
11
11
  /** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
12
- export declare function isKimiK26ModelId(modelId: string): boolean;
12
+ export declare const isKimiK26ModelId: (modelId: string) => boolean;
13
13
  /** Claude ids in any namespace form (`claude-*`, `vendor/claude.x`). */
14
- export declare function isClaudeModelId(modelId: string): boolean;
14
+ export declare const isClaudeModelId: (modelId: string) => boolean;
15
15
  /** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
16
- export declare function isAnthropicNamespacedModelId(modelId: string): boolean;
16
+ export declare const isAnthropicNamespacedModelId: (modelId: string) => boolean;
17
17
  /** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
18
- export declare function isQwenModelId(modelId: string): boolean;
18
+ export declare const isQwenModelId: (modelId: string) => boolean;
19
19
  /** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
20
- export declare function isGemmaModelId(modelId: string): boolean;
20
+ export declare const isGemmaModelId: (modelId: string) => boolean;
21
21
  /** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
22
- export declare function isDeepseekModelIdOrName(value: string): boolean;
22
+ export declare const isDeepseekModelIdOrName: (modelId: string) => boolean;
23
23
  /** Xiaomi MiMo family by id or display name. */
24
- export declare function isMimoModelIdOrName(value: string): boolean;
24
+ export declare const isMimoModelIdOrName: (modelId: string) => boolean;
25
25
  /**
26
26
  * Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
27
27
  * (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
28
28
  * param, so callers must omit reasoning effort for them.
29
29
  */
30
- export declare function isGrokReasoningEffortCapable(modelId: string): boolean;
30
+ export declare const isGrokReasoningEffortCapable: (modelId: string) => boolean;
31
31
  /**
32
32
  * MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
33
33
  * `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
@@ -37,18 +37,18 @@ export declare function isGrokReasoningEffortCapable(modelId: string): boolean;
37
37
  * `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
38
38
  * clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
39
39
  */
40
- export declare function isMinimaxM2FamilyModelId(modelId: string): boolean;
40
+ export declare const isMinimaxM2FamilyModelId: (modelId: string) => boolean;
41
41
  /** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
42
- export declare function isMinimaxM3FamilyModelId(modelId: string): boolean;
42
+ export declare const isMinimaxM3FamilyModelId: (modelId: string) => boolean;
43
43
  /**
44
44
  * OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
45
45
  * `vendor/gpt-oss-…`). The Harmony reasoning format only accepts
46
46
  * `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
47
47
  * and `none`.
48
48
  */
49
- export declare function isOpenAIGptOssModelId(modelId: string): boolean;
49
+ export declare const isOpenAIGptOssModelId: (modelId: string) => boolean;
50
50
  /** OpenAI model ids (gpt-*, o1-*, o3-*, o4-*, or prefixed with openai/). */
51
- export declare function isOpenAIModelId(modelId: string): boolean;
51
+ export declare const isOpenAIModelId: (modelId: string) => boolean;
52
52
  /**
53
53
  * Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
54
54
  * `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
@@ -56,11 +56,11 @@ export declare function isOpenAIModelId(modelId: string): boolean;
56
56
  * keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
57
57
  * allowlist.
58
58
  */
59
- export declare function isReasoningGlmModelId(modelId: string): boolean;
59
+ export declare const isReasoningGlmModelId: (modelId: string) => boolean;
60
60
  /** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
61
- export declare function isGlm52ReasoningEffortModelId(modelId: string): boolean;
61
+ export declare const isGlm52ReasoningEffortModelId: (modelId: string) => boolean;
62
62
  /** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
63
- export declare function isGlmVisionModelId(modelId: string): boolean;
63
+ export declare const isGlmVisionModelId: (modelId: string) => boolean;
64
64
  /**
65
65
  * Coarse vendor-lineage token for "are two models the same family?" checks
66
66
  * (e.g. picking a cross-family reviewer). All Claude point releases share a token,
@@ -72,7 +72,7 @@ export declare function isGlmVisionModelId(modelId: string): boolean;
72
72
  * Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
73
73
  * collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
74
74
  */
75
- export declare function modelFamilyToken(modelId: string): string;
75
+ export declare const modelFamilyToken: (modelId: string) => string;
76
76
  /**
77
77
  * Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
78
78
  * the Claude Fable/Mythos 5 generation. Older adaptive-thinking models
@@ -80,13 +80,13 @@ export declare function modelFamilyToken(modelId: string): string;
80
80
  * dashed version forms both match while bare dated ids
81
81
  * (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
82
82
  */
83
- export declare function supportsAdaptiveThinkingDisplay(modelId: string): boolean;
83
+ export declare const supportsAdaptiveThinkingDisplay: (modelId: string) => boolean;
84
84
  /**
85
85
  * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
86
86
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
87
87
  * - Thinking content is omitted by default (needs display: "summarized")
88
88
  */
89
- export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
89
+ export declare const hasOpus47ApiRestrictions: (modelId: string) => boolean;
90
90
  /**
91
91
  * Mid-conversation `role: "system"` messages (system instructions appended at
92
92
  * non-first positions in the `messages` array) are supported starting with
@@ -94,8 +94,8 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
94
94
  * models reject the role.
95
95
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
96
96
  */
97
- export declare function supportsMidConversationSystemMessages(modelId: string): boolean;
98
- export declare function isAnthropicFableOrMythosModel(modelId: string): boolean;
97
+ export declare const supportsMidConversationSystemMessages: (modelId: string) => boolean;
98
+ export declare const isAnthropicFableOrMythosModel: (modelId: string) => boolean;
99
99
  /** Thinking-variant token location inside a model id. */
100
100
  export interface ThinkingVariantToken {
101
101
  index: number;
@@ -115,4 +115,4 @@ export declare function findThinkingVariantToken(modelId: string): ThinkingVaria
115
115
  * token exists or nothing would remain. Callers MUST verify the result names
116
116
  * a live model.
117
117
  */
118
- export declare function stripThinkingVariantToken(modelId: string): string | undefined;
118
+ export declare const stripThinkingVariantToken: (modelId: string) => string | undefined;
@@ -63,7 +63,7 @@ export declare function mapEffortToGoogleThinkingLevel(effort: Effort): "MINIMAL
63
63
  * Maps a normalized thinking effort to Anthropic adaptive effort values via
64
64
  * the model's baked `thinking.effortMap` (identity for unmapped efforts).
65
65
  */
66
- export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): "low" | "medium" | "high" | "xhigh" | "max";
66
+ export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive";
67
67
  /**
68
68
  * Resolves the upstream wire model id for a request at the given effort
69
69
  * (`undefined` = thinking off). Collapsed effort-tier variants route through
@@ -265,6 +265,18 @@ export interface KimiCodeModelManagerConfig {
265
265
  fetch?: FetchImpl;
266
266
  }
267
267
  export declare function kimiCodeModelManagerOptions(config?: KimiCodeModelManagerConfig): ModelManagerOptions<"openai-completions">;
268
+ /** Native LM Studio metadata keyed by model id from `/api/v0/models`. */
269
+ export interface LmStudioNativeModelMetadata {
270
+ input: ("text" | "image")[];
271
+ contextWindow?: number;
272
+ }
273
+ /** Options for LM Studio's optional native metadata probe. */
274
+ export interface LmStudioNativeModelMetadataOptions {
275
+ headers?: Record<string, string>;
276
+ signal?: AbortSignal;
277
+ }
278
+ /** Fetches LM Studio native model metadata used to mark VLM models as image-capable. */
279
+ export declare function fetchLmStudioNativeModelMetadata(baseUrl: string, fetchImpl?: FetchImpl, options?: LmStudioNativeModelMetadataOptions): Promise<Map<string, LmStudioNativeModelMetadata> | null>;
268
280
  export interface LmStudioModelManagerConfig {
269
281
  apiKey?: string;
270
282
  baseUrl?: string;
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.0.6",
4
+ "version": "16.0.8",
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.0.6",
37
+ "@oh-my-pi/pi-utils": "16.0.8",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.0.6",
42
+ "@oh-my-pi/pi-ai": "16.0.8",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
package/src/hosts.ts CHANGED
@@ -16,7 +16,7 @@ interface HostClassSpec {
16
16
  readonly providers?: readonly string[];
17
17
  /** Provider-id prefixes that imply this host class (e.g. `xiaomi-token-plan-`). */
18
18
  readonly providerPrefixes?: readonly string[];
19
- /** Case-insensitive substrings matched against the base URL. */
19
+ /** Lowercase ASCII substrings matched case-insensitively against the base URL. */
20
20
  readonly urlMarkers: readonly string[];
21
21
  // Strict hostname matching is intentionally not modeled here: the one
22
22
  // auth-sensitive consumer (Anthropic official-endpoint) parses the URL
@@ -68,9 +68,8 @@ export type KnownHost = keyof typeof KNOWN_HOSTS;
68
68
  export function hostMatchesUrl(baseUrl: string | undefined, host: KnownHost): boolean {
69
69
  if (!baseUrl) return false;
70
70
  const spec: HostClassSpec = KNOWN_HOSTS[host];
71
- const normalized = baseUrl.toLowerCase();
72
71
  for (const marker of spec.urlMarkers) {
73
- if (normalized.includes(marker)) return true;
72
+ if (includesAsciiCaseInsensitive(baseUrl, marker)) return true;
74
73
  }
75
74
  return false;
76
75
  }
@@ -91,6 +90,19 @@ export function modelMatchesHost(model: { provider: string; baseUrl: string }, h
91
90
  return hostMatchesUrl(model.baseUrl, host);
92
91
  }
93
92
 
93
+ function includesAsciiCaseInsensitive(value: string, lowerNeedle: string): boolean {
94
+ const needleLength = lowerNeedle.length;
95
+ const end = value.length - needleLength;
96
+ for (let start = 0; start <= end; start++) {
97
+ let offset = 0;
98
+ for (; offset < needleLength; offset++) {
99
+ if ((value.charCodeAt(start + offset) | 0x20) !== lowerNeedle.charCodeAt(offset)) break;
100
+ }
101
+ if (offset === needleLength) return true;
102
+ }
103
+ return false;
104
+ }
105
+
94
106
  // --- Endpoint-shape predicates (URL path/verb shapes, not vendor hosts) ---
95
107
 
96
108
  /** Vertex AI express-mode OpenAI-compatible endpoint (`…/endpoints/openapi`). */
@@ -51,9 +51,15 @@ export interface UnknownModel {
51
51
  export type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
52
52
 
53
53
  /** Strip a provider namespace prefix (`openai/gpt-5.4` → `gpt-5.4`). */
54
+ // Cache keyed by model id (a bounded set of bundled/aggregator ids), so no eviction is needed.
55
+ const bareModelIdCache = new Map<string, string>();
54
56
  export function bareModelId(modelId: string): string {
57
+ const cached = bareModelIdCache.get(modelId);
58
+ if (cached !== undefined) return cached;
55
59
  const p = modelId.lastIndexOf("/");
56
- return p !== -1 ? modelId.slice(p + 1) : modelId;
60
+ const result = p !== -1 ? modelId.slice(p + 1) : modelId;
61
+ bareModelIdCache.set(modelId, result);
62
+ return result;
57
63
  }
58
64
 
59
65
  export function parseKnownModel(modelId: string): ParsedModel {
@@ -16,45 +16,58 @@ import {
16
16
  semverGte,
17
17
  } from "./classify";
18
18
 
19
+ /** Bounded process-lifetime cache memo helper. */
20
+ function memo<T>(fn: (modelId: string) => T): (modelId: string) => T {
21
+ const cache = new Map<string, T>();
22
+ return (modelId: string) => {
23
+ if (cache.has(modelId)) {
24
+ return cache.get(modelId) as T;
25
+ }
26
+ const result = fn(modelId);
27
+ cache.set(modelId, result);
28
+ return result;
29
+ };
30
+ }
31
+
19
32
  /** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
20
- export function isKimiModelId(modelId: string): boolean {
33
+ export const isKimiModelId = memo((modelId: string): boolean => {
21
34
  return modelId.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(modelId);
22
- }
35
+ });
23
36
 
24
37
  /** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
25
- export function isKimiK26ModelId(modelId: string): boolean {
38
+ export const isKimiK26ModelId = memo((modelId: string): boolean => {
26
39
  return /(^|\/)kimi-k2(?:\.6|p6)(?:[-:]|$)/i.test(modelId);
27
- }
40
+ });
28
41
 
29
42
  /** Claude ids in any namespace form (`claude-*`, `vendor/claude.x`). */
30
- export function isClaudeModelId(modelId: string): boolean {
43
+ export const isClaudeModelId = memo((modelId: string): boolean => {
31
44
  return /(^|\/)claude[-.]/i.test(modelId);
32
- }
45
+ });
33
46
 
34
47
  /** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
35
- export function isAnthropicNamespacedModelId(modelId: string): boolean {
48
+ export const isAnthropicNamespacedModelId = memo((modelId: string): boolean => {
36
49
  return /(^|\/)anthropic\//i.test(modelId);
37
- }
50
+ });
38
51
 
39
52
  /** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
40
- export function isQwenModelId(modelId: string): boolean {
53
+ export const isQwenModelId = memo((modelId: string): boolean => {
41
54
  return modelId.toLowerCase().includes("qwen");
42
- }
55
+ });
43
56
 
44
57
  /** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
45
- export function isGemmaModelId(modelId: string): boolean {
58
+ export const isGemmaModelId = memo((modelId: string): boolean => {
46
59
  return /(^|\/)gemma[-.]?\d/i.test(modelId);
47
- }
60
+ });
48
61
 
49
62
  /** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
50
- export function isDeepseekModelIdOrName(value: string): boolean {
63
+ export const isDeepseekModelIdOrName = memo((value: string): boolean => {
51
64
  return value.toLowerCase().includes("deepseek");
52
- }
65
+ });
53
66
 
54
67
  /** Xiaomi MiMo family by id or display name. */
55
- export function isMimoModelIdOrName(value: string): boolean {
68
+ export const isMimoModelIdOrName = memo((value: string): boolean => {
56
69
  return value.toLowerCase().includes("mimo");
57
- }
70
+ });
58
71
 
59
72
  const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3"] as const;
60
73
 
@@ -63,11 +76,11 @@ const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "g
63
76
  * (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
64
77
  * param, so callers must omit reasoning effort for them.
65
78
  */
66
- export function isGrokReasoningEffortCapable(modelId: string): boolean {
79
+ export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
67
80
  const bare = bareModelId(modelId).trim().toLowerCase();
68
81
  if (!bare) return false;
69
82
  return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
70
- }
83
+ });
71
84
 
72
85
  /**
73
86
  * MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
@@ -78,20 +91,20 @@ export function isGrokReasoningEffortCapable(modelId: string): boolean {
78
91
  * `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
79
92
  * clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
80
93
  */
81
- export function isMinimaxM2FamilyModelId(modelId: string): boolean {
94
+ export const isMinimaxM2FamilyModelId = memo((modelId: string): boolean => {
82
95
  const lower = modelId.toLowerCase();
83
96
  if (!lower.includes("minimax")) return false;
84
97
  // Boundary-delimited `m2` token followed by zero or more digits (dotless
85
98
  // variants like `m21`/`m25`/`m27`) and an optional dotted minor version.
86
99
  return /(?:^|[/.-])m2\d*(?:[.-]\d+)?(?:[-.:_]|$)/i.test(lower);
87
- }
100
+ });
88
101
 
89
102
  /** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
90
- export function isMinimaxM3FamilyModelId(modelId: string): boolean {
103
+ export const isMinimaxM3FamilyModelId = memo((modelId: string): boolean => {
91
104
  const lower = modelId.toLowerCase();
92
105
  if (!lower.includes("minimax")) return false;
93
106
  return /(?:^|[/._-])(?:minimax[/._-])?m3(?:[-.:_]|$)/i.test(lower);
94
- }
107
+ });
95
108
 
96
109
  /**
97
110
  * OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
@@ -99,14 +112,14 @@ export function isMinimaxM3FamilyModelId(modelId: string): boolean {
99
112
  * `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
100
113
  * and `none`.
101
114
  */
102
- export function isOpenAIGptOssModelId(modelId: string): boolean {
115
+ export const isOpenAIGptOssModelId = memo((modelId: string): boolean => {
103
116
  return /(^|\/)gpt-oss[-:]/i.test(modelId);
104
- }
117
+ });
105
118
 
106
119
  /** OpenAI model ids (gpt-*, o1-*, o3-*, o4-*, or prefixed with openai/). */
107
- export function isOpenAIModelId(modelId: string): boolean {
120
+ export const isOpenAIModelId = memo((modelId: string): boolean => {
108
121
  return /(^|\/)(gpt|o1|o3|o4)[-.]/i.test(modelId) || modelId.toLowerCase().includes("openai/");
109
- }
122
+ });
110
123
 
111
124
  /**
112
125
  * Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
@@ -115,7 +128,7 @@ export function isOpenAIModelId(modelId: string): boolean {
115
128
  * keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
116
129
  * allowlist.
117
130
  */
118
- export function isReasoningGlmModelId(modelId: string): boolean {
131
+ export const isReasoningGlmModelId = memo((modelId: string): boolean => {
119
132
  const glm = parseGlmModel(bareModelId(modelId));
120
133
  if (!glm || glm.vision) {
121
134
  return false;
@@ -124,9 +137,10 @@ export function isReasoningGlmModelId(modelId: string): boolean {
124
137
  return false;
125
138
  }
126
139
  return semverGte(glm.version, "4.5");
127
- }
140
+ });
141
+
128
142
  /** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
129
- export function isGlm52ReasoningEffortModelId(modelId: string): boolean {
143
+ export const isGlm52ReasoningEffortModelId = memo((modelId: string): boolean => {
130
144
  const glm = parseGlmModel(bareModelId(modelId));
131
145
  if (!glm || glm.vision) {
132
146
  return false;
@@ -135,12 +149,13 @@ export function isGlm52ReasoningEffortModelId(modelId: string): boolean {
135
149
  return false;
136
150
  }
137
151
  return semverGte(glm.version, "5.2");
138
- }
152
+ });
139
153
 
140
154
  /** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
141
- export function isGlmVisionModelId(modelId: string): boolean {
155
+ export const isGlmVisionModelId = memo((modelId: string): boolean => {
142
156
  return parseGlmModel(bareModelId(modelId))?.vision === true;
143
- }
157
+ });
158
+
144
159
  /**
145
160
  * Coarse vendor-lineage token for "are two models the same family?" checks
146
161
  * (e.g. picking a cross-family reviewer). All Claude point releases share a token,
@@ -152,7 +167,7 @@ export function isGlmVisionModelId(modelId: string): boolean {
152
167
  * Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
153
168
  * collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
154
169
  */
155
- export function modelFamilyToken(modelId: string): string {
170
+ export const modelFamilyToken = memo((modelId: string): string => {
156
171
  const parsed = parseKnownModel(modelId);
157
172
  if (parsed.family !== "unknown") return parsed.family;
158
173
  if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
@@ -166,7 +181,7 @@ export function modelFamilyToken(modelId: string): string {
166
181
  if (isGemmaModelId(modelId)) return "gemma";
167
182
  if (parseGlmModel(bareModelId(modelId))) return "glm";
168
183
  return "";
169
- }
184
+ });
170
185
 
171
186
  /**
172
187
  * Adaptive thinking `display` is supported starting with Claude Opus 4.7 and
@@ -175,23 +190,23 @@ export function modelFamilyToken(modelId: string): string {
175
190
  * dashed version forms both match while bare dated ids
176
191
  * (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
177
192
  */
178
- export function supportsAdaptiveThinkingDisplay(modelId: string): boolean {
193
+ export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
179
194
  const parsed = parseAnthropicModel(bareModelId(modelId));
180
195
  if (!parsed) return false;
181
196
  if (isFableOrMythos(parsed.kind)) return semverGte(parsed.version, "5");
182
197
  return parsed.kind === "opus" && semverGte(parsed.version, "4.7");
183
- }
198
+ });
184
199
 
185
200
  /**
186
201
  * Returns true for Anthropic models with Opus 4.7+/Fable/Mythos API restrictions:
187
202
  * - Sampling parameters (temperature/top_p/top_k) return 400 error
188
203
  * - Thinking content is omitted by default (needs display: "summarized")
189
204
  */
190
- export function hasOpus47ApiRestrictions(modelId: string): boolean {
205
+ export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
191
206
  const parsed = parseAnthropicModel(bareModelId(modelId));
192
207
  if (!parsed) return false;
193
208
  return (parsed.kind === "opus" && semverGte(parsed.version, "4.7")) || isFableOrMythos(parsed.kind);
194
- }
209
+ });
195
210
 
196
211
  /**
197
212
  * Mid-conversation `role: "system"` messages (system instructions appended at
@@ -200,16 +215,16 @@ export function hasOpus47ApiRestrictions(modelId: string): boolean {
200
215
  * models reject the role.
201
216
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
202
217
  */
203
- export function supportsMidConversationSystemMessages(modelId: string): boolean {
218
+ export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
204
219
  const parsed = parseAnthropicModel(bareModelId(modelId));
205
220
  if (!parsed) return false;
206
221
  return (parsed.kind === "opus" && semverGte(parsed.version, "4.8")) || isFableOrMythos(parsed.kind);
207
- }
222
+ });
208
223
 
209
- export function isAnthropicFableOrMythosModel(modelId: string): boolean {
224
+ export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
210
225
  const parsed = parseAnthropicModel(bareModelId(modelId));
211
226
  return parsed !== null && isFableOrMythos(parsed.kind);
212
- }
227
+ });
213
228
 
214
229
  /** Thinking-variant token location inside a model id. */
215
230
  export interface ThinkingVariantToken {
@@ -245,9 +260,9 @@ export function findThinkingVariantToken(modelId: string): ThinkingVariantToken
245
260
  * token exists or nothing would remain. Callers MUST verify the result names
246
261
  * a live model.
247
262
  */
248
- export function stripThinkingVariantToken(modelId: string): string | undefined {
263
+ export const stripThinkingVariantToken = memo((modelId: string): string | undefined => {
249
264
  const token = findThinkingVariantToken(modelId);
250
265
  if (!token) return undefined;
251
266
  const stripped = modelId.slice(0, token.index) + modelId.slice(token.index + token.length);
252
267
  return stripped.length > 0 ? stripped : undefined;
253
- }
268
+ });
@@ -25,6 +25,7 @@ import {
25
25
  isDeepseekModelIdOrName,
26
26
  isGlm52ReasoningEffortModelId,
27
27
  isMinimaxM2FamilyModelId,
28
+ isMinimaxM3FamilyModelId,
28
29
  isOpenAIGptOssModelId,
29
30
  supportsAdaptiveThinkingDisplay,
30
31
  } from "./identity/family";
@@ -113,6 +114,12 @@ export const ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER: Readonly<Partial<Record<Effor
113
114
  [Effort.XHigh]: "max",
114
115
  };
115
116
 
117
+ const MINIMAX_ANTHROPIC_ADAPTIVE_EFFORT_MAP: Readonly<EffortMap> = {
118
+ [Effort.Low]: "adaptive",
119
+ [Effort.Medium]: "adaptive",
120
+ [Effort.High]: "adaptive",
121
+ };
122
+
116
123
  // ---------------------------------------------------------------------------
117
124
  // Build-time derivation (buildModel + catalog generator only)
118
125
  // ---------------------------------------------------------------------------
@@ -295,6 +302,10 @@ function isOllamaCloudGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpe
295
302
  return spec.api === "ollama-chat" && spec.provider === "ollama-cloud" && isGlm52ReasoningEffortModelId(spec.id);
296
303
  }
297
304
 
305
+ function isMinimaxReasoningModelOnAnthropicEndpoint<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
306
+ return spec.api === "anthropic-messages" && (isMinimaxM2FamilyModelId(spec.id) || isMinimaxM3FamilyModelId(spec.id));
307
+ }
308
+
298
309
  function readCompatEffortMap(compat: CompatOf<Api>): EffortMap | undefined {
299
310
  if (compat === undefined || !("reasoningEffortMap" in compat)) {
300
311
  return undefined;
@@ -309,6 +320,9 @@ function inferDetectedEffortMap<TApi extends Api>(
309
320
  mode: ThinkingConfig["mode"],
310
321
  ): EffortMap | undefined {
311
322
  if (mode === "anthropic-adaptive") {
323
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
324
+ return MINIMAX_ANTHROPIC_ADAPTIVE_EFFORT_MAP;
325
+ }
312
326
  return anthropicModelHasRealXHighEffort(spec, parsedModel)
313
327
  ? ANTHROPIC_ADAPTIVE_EFFORT_MAP_5_TIER
314
328
  : ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER;
@@ -446,6 +460,9 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
446
460
  }
447
461
 
448
462
  function inferFallbackEfforts<TApi extends Api>(spec: ModelSpec<TApi>, compat: CompatOf<TApi>): readonly Effort[] {
463
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
464
+ return LOW_MEDIUM_HIGH_REASONING_EFFORTS;
465
+ }
449
466
  if (spec.api === "anthropic-messages") {
450
467
  return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
451
468
  }
@@ -488,6 +505,9 @@ function inferThinkingControlMode<TApi extends Api>(
488
505
  : "budget";
489
506
 
490
507
  case "anthropic-messages":
508
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
509
+ return "anthropic-adaptive";
510
+ }
491
511
  if (parsedModel.family === "anthropic") {
492
512
  if (semverGte(parsedModel.version, "4.6")) {
493
513
  return "anthropic-adaptive";
@@ -626,9 +646,15 @@ export function mapEffortToGoogleThinkingLevel(effort: Effort): "MINIMAL" | "LOW
626
646
  export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
627
647
  model: ApiModel<TApi>,
628
648
  effort: Effort,
629
- ): "low" | "medium" | "high" | "xhigh" | "max" {
649
+ ): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive" {
630
650
  const supported = requireSupportedEffort(model, effort);
631
- return (model.thinking?.effortMap?.[supported] ?? supported) as "low" | "medium" | "high" | "xhigh" | "max";
651
+ return (model.thinking?.effortMap?.[supported] ?? supported) as
652
+ | "low"
653
+ | "medium"
654
+ | "high"
655
+ | "xhigh"
656
+ | "max"
657
+ | "adaptive";
632
658
  }
633
659
 
634
660
  /**