@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 +16 -0
- package/dist/types/identity/classify.d.ts +0 -1
- package/dist/types/identity/family.d.ts +22 -22
- package/dist/types/model-thinking.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +12 -0
- package/package.json +3 -3
- package/src/hosts.ts +15 -3
- package/src/identity/classify.ts +7 -1
- package/src/identity/family.ts +59 -44
- package/src/model-thinking.ts +28 -2
- package/src/models.json +970 -117
- package/src/provider-models/openai-compat.ts +105 -3
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
|
|
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
|
|
12
|
+
export declare const isKimiK26ModelId: (modelId: string) => boolean;
|
|
13
13
|
/** Claude ids in any namespace form (`claude-*`, `vendor/claude.x`). */
|
|
14
|
-
export declare
|
|
14
|
+
export declare const isClaudeModelId: (modelId: string) => boolean;
|
|
15
15
|
/** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
|
|
16
|
-
export declare
|
|
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
|
|
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
|
|
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
|
|
22
|
+
export declare const isDeepseekModelIdOrName: (modelId: string) => boolean;
|
|
23
23
|
/** Xiaomi MiMo family by id or display name. */
|
|
24
|
-
export declare
|
|
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
|
|
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
|
|
40
|
+
export declare const isMinimaxM2FamilyModelId: (modelId: string) => boolean;
|
|
41
41
|
/** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
|
|
42
|
-
export declare
|
|
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
|
|
49
|
+
export declare const isOpenAIGptOssModelId: (modelId: string) => boolean;
|
|
50
50
|
/** OpenAI model ids (gpt-*, o1-*, o3-*, o4-*, or prefixed with openai/). */
|
|
51
|
-
export declare
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
98
|
-
export declare
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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 (
|
|
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`). */
|
package/src/identity/classify.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/identity/family.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|
package/src/model-thinking.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|