@linxiraos/pi-catalog 1.0.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 +999 -0
- package/README.md +43 -0
- package/dist/types/build.d.ts +14 -0
- package/dist/types/compat/anthropic.d.ts +18 -0
- package/dist/types/compat/apply.d.ts +7 -0
- package/dist/types/compat/bedrock.d.ts +3 -0
- package/dist/types/compat/devin.d.ts +9 -0
- package/dist/types/compat/openai.d.ts +26 -0
- package/dist/types/discovery/antigravity.d.ts +70 -0
- package/dist/types/discovery/codex.d.ts +36 -0
- package/dist/types/discovery/cursor-gen/agent_pb.d.ts +16769 -0
- package/dist/types/discovery/cursor.d.ts +23 -0
- package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +1715 -0
- package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +693 -0
- package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +9158 -0
- package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +52 -0
- package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +276 -0
- package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +78 -0
- package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +999 -0
- package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +1704 -0
- package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +656 -0
- package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +15014 -0
- package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +607 -0
- package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +11830 -0
- package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +248 -0
- package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +1747 -0
- package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +509 -0
- package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +9048 -0
- package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +1760 -0
- package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +286 -0
- package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +405 -0
- package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +582 -0
- package/dist/types/discovery/devin.d.ts +24 -0
- package/dist/types/discovery/gemini.d.ts +25 -0
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +5 -0
- package/dist/types/discovery/openai-compatible.d.ts +74 -0
- package/dist/types/effort.d.ts +10 -0
- package/dist/types/fireworks-model-id.d.ts +23 -0
- package/dist/types/hosts.d.ts +164 -0
- package/dist/types/identity/bundled.d.ts +3 -0
- package/dist/types/identity/classify.d.ts +69 -0
- package/dist/types/identity/dialect.d.ts +3 -0
- package/dist/types/identity/family.d.ts +181 -0
- package/dist/types/identity/id.d.ts +12 -0
- package/dist/types/identity/index.d.ts +8 -0
- package/dist/types/identity/markers.d.ts +4 -0
- package/dist/types/identity/priority.d.ts +1 -0
- package/dist/types/identity/reference.d.ts +29 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/model-cache.d.ts +23 -0
- package/dist/types/model-manager.d.ts +77 -0
- package/dist/types/model-thinking.d.ts +66 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-models/bundled-references.d.ts +13 -0
- package/dist/types/provider-models/cache-provider-id.d.ts +9 -0
- package/dist/types/provider-models/descriptor-types.d.ts +76 -0
- package/dist/types/provider-models/descriptors.d.ts +504 -0
- package/dist/types/provider-models/google.d.ts +27 -0
- package/dist/types/provider-models/index.d.ts +7 -0
- package/dist/types/provider-models/ollama.d.ts +22 -0
- package/dist/types/provider-models/openai-compat.d.ts +599 -0
- package/dist/types/provider-models/special.d.ts +53 -0
- package/dist/types/types.d.ts +811 -0
- package/dist/types/utils.d.ts +20 -0
- package/dist/types/variant-collapse.d.ts +133 -0
- package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
- package/dist/types/wire/codex.d.ts +42 -0
- package/dist/types/wire/coreweave.d.ts +12 -0
- package/dist/types/wire/gemini-headers.d.ts +37 -0
- package/dist/types/wire/github-copilot.d.ts +39 -0
- package/package.json +101 -0
- package/src/build.ts +94 -0
- package/src/compat/anthropic.ts +179 -0
- package/src/compat/apply.ts +15 -0
- package/src/compat/bedrock.ts +153 -0
- package/src/compat/devin.ts +12 -0
- package/src/compat/openai.ts +796 -0
- package/src/discovery/antigravity.ts +253 -0
- package/src/discovery/codex.ts +346 -0
- package/src/discovery/cursor-gen/agent_pb.ts +19513 -0
- package/src/discovery/cursor.ts +396 -0
- package/src/discovery/devin-gen/buf/validate/validate_pb.ts +1862 -0
- package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +871 -0
- package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +11083 -0
- package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +71 -0
- package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +348 -0
- package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +103 -0
- package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +1198 -0
- package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +2063 -0
- package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +810 -0
- package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +18354 -0
- package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +732 -0
- package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +14277 -0
- package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +312 -0
- package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +2106 -0
- package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +623 -0
- package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +10918 -0
- package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +2125 -0
- package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +361 -0
- package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +450 -0
- package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +712 -0
- package/src/discovery/devin.ts +151 -0
- package/src/discovery/gemini.ts +260 -0
- package/src/discovery/gitlab-duo-workflow.ts +859 -0
- package/src/discovery/index.ts +5 -0
- package/src/discovery/openai-compatible.ts +249 -0
- package/src/effort.ts +18 -0
- package/src/fireworks-model-id.ts +50 -0
- package/src/hosts.ts +155 -0
- package/src/identity/bundled.ts +28 -0
- package/src/identity/classify.ts +219 -0
- package/src/identity/dialect.ts +42 -0
- package/src/identity/family.ts +366 -0
- package/src/identity/id.ts +81 -0
- package/src/identity/index.ts +8 -0
- package/src/identity/markers.ts +49 -0
- package/src/identity/priority.ts +59 -0
- package/src/identity/reference.ts +167 -0
- package/src/index.ts +17 -0
- package/src/model-cache.ts +285 -0
- package/src/model-manager.ts +672 -0
- package/src/model-thinking.ts +821 -0
- package/src/models.json +106099 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +93 -0
- package/src/provider-models/bundled-references.ts +75 -0
- package/src/provider-models/cache-provider-id.ts +68 -0
- package/src/provider-models/descriptor-types.ts +85 -0
- package/src/provider-models/descriptors.ts +584 -0
- package/src/provider-models/google.ts +107 -0
- package/src/provider-models/index.ts +7 -0
- package/src/provider-models/ollama.ts +204 -0
- package/src/provider-models/openai-compat.ts +5991 -0
- package/src/provider-models/special.ts +213 -0
- package/src/types.ts +952 -0
- package/src/utils.ts +69 -0
- package/src/variant-collapse.ts +1225 -0
- package/src/wire/alibaba-token-plan.ts +54 -0
- package/src/wire/codex.ts +60 -0
- package/src/wire/coreweave.ts +51 -0
- package/src/wire/gemini-headers.ts +74 -0
- package/src/wire/github-copilot.ts +115 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-id classification: parse a model id into its family (gemini / anthropic /
|
|
3
|
+
* openai), kind/variant, and version. This is the shared layer both catalog
|
|
4
|
+
* policy rules (`model-thinking.ts`) and downstream consumers build on —
|
|
5
|
+
* classification lives here, the rules that consume it stay with their domain.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type SemVer = {
|
|
9
|
+
major: number;
|
|
10
|
+
minor: number;
|
|
11
|
+
patch: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type GeminiKind = "pro" | "flash";
|
|
15
|
+
export type AnthropicKind = "opus" | "sonnet" | "fable" | "mythos";
|
|
16
|
+
export type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "mini" | "max" | "nano";
|
|
17
|
+
export type GlmVariant = "base" | "air" | "turbo" | "flash" | "flashx" | "preview";
|
|
18
|
+
|
|
19
|
+
export interface GeminiModel {
|
|
20
|
+
family: "gemini";
|
|
21
|
+
kind: GeminiKind;
|
|
22
|
+
version: SemVer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AnthropicModel {
|
|
26
|
+
family: "anthropic";
|
|
27
|
+
kind: AnthropicKind;
|
|
28
|
+
version: SemVer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OpenAIModel {
|
|
32
|
+
family: "openai";
|
|
33
|
+
variant: OpenAIVariant;
|
|
34
|
+
version: SemVer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GlmModel {
|
|
38
|
+
family: "glm";
|
|
39
|
+
/** Suffix variant (`-air`, `-turbo`, `-flash`, `-flashx`, `-preview`); `base` when none. */
|
|
40
|
+
variant: GlmVariant;
|
|
41
|
+
/** Vision SKU — the `v` that attaches directly to the version (`glm-4v`, `glm-4.5v`). */
|
|
42
|
+
vision: boolean;
|
|
43
|
+
version: SemVer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UnknownModel {
|
|
47
|
+
family: "unknown";
|
|
48
|
+
id: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel;
|
|
52
|
+
|
|
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>();
|
|
56
|
+
export function bareModelId(modelId: string): string {
|
|
57
|
+
const cached = bareModelIdCache.get(modelId);
|
|
58
|
+
if (cached !== undefined) return cached;
|
|
59
|
+
const p = modelId.lastIndexOf("/");
|
|
60
|
+
const result = p !== -1 ? modelId.slice(p + 1) : modelId;
|
|
61
|
+
bareModelIdCache.set(modelId, result);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseKnownModel(modelId: string): ParsedModel {
|
|
66
|
+
const canonicalId = bareModelId(modelId);
|
|
67
|
+
return (
|
|
68
|
+
parseGeminiModel(canonicalId) ??
|
|
69
|
+
parseAnthropicModel(canonicalId) ??
|
|
70
|
+
parseOpenAIModel(canonicalId) ?? { family: "unknown", id: canonicalId }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wrap a parse function in a per-id memo cache. Caches the `null` result too, so
|
|
76
|
+
* repeated misses (the common case — ids of other families) stay O(1) and never
|
|
77
|
+
* re-run the regex/semver work.
|
|
78
|
+
*/
|
|
79
|
+
function parser<T>(parse: (modelId: string) => T | null): (modelId: string) => T | null {
|
|
80
|
+
const cache = new Map<string, T | null>();
|
|
81
|
+
return modelId => {
|
|
82
|
+
const hit = cache.get(modelId);
|
|
83
|
+
if (hit !== undefined || cache.has(modelId)) {
|
|
84
|
+
return hit ?? null;
|
|
85
|
+
}
|
|
86
|
+
const result = parse(modelId);
|
|
87
|
+
cache.set(modelId, result);
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const GEMINI_SUFFIX = "-preview";
|
|
93
|
+
export const parseGeminiModel = parser((modelId): GeminiModel | null => {
|
|
94
|
+
if (modelId.endsWith(GEMINI_SUFFIX)) {
|
|
95
|
+
modelId = modelId.slice(0, -GEMINI_SUFFIX.length);
|
|
96
|
+
}
|
|
97
|
+
const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash)\b/.exec(modelId);
|
|
98
|
+
if (!match) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const version = parseSemVer(match[1]);
|
|
102
|
+
if (!version) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return { family: "gemini", kind: match[2] as GeminiKind, version };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const parseAnthropicModel = parser((modelId): AnthropicModel | null => {
|
|
109
|
+
const kindFirst = /claude-(opus|sonnet|fable|mythos)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
|
|
110
|
+
const versionFirst = kindFirst
|
|
111
|
+
? null
|
|
112
|
+
: /claude-(\d{1,2}(?:[.-]\d{1,2}){0,2})-(opus|sonnet|fable|mythos)\b/.exec(modelId);
|
|
113
|
+
const kind = kindFirst?.[1] ?? versionFirst?.[2];
|
|
114
|
+
const versionInput = kindFirst?.[2] ?? versionFirst?.[1];
|
|
115
|
+
if (!kind || !versionInput) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const version = parseSemVer(versionInput);
|
|
119
|
+
if (!version) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return { family: "anthropic", kind: kind as AnthropicKind, version };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
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) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const version = parseSemVer(match[1]);
|
|
131
|
+
if (!version) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse a GLM (Zhipu / Z.AI) model id into family + variant + vision + version.
|
|
139
|
+
* Shape: `glm-<version>[v][-<variant>]` — e.g. `glm-4.5`, `glm-4.5-air`,
|
|
140
|
+
* `glm-5-turbo`, `glm-4.5v`, `glm-5-preview`. The `v` (vision) attaches to the
|
|
141
|
+
* version; other variants are `-` suffixes. Standalone like `parseAnthropicModel`
|
|
142
|
+
* is used in family.ts — GLM needs no global thinking policy, so it stays out of
|
|
143
|
+
* `parseKnownModel`.
|
|
144
|
+
*/
|
|
145
|
+
export const parseGlmModel = parser((modelId): GlmModel | null => {
|
|
146
|
+
const match = /glm-(\d{1,2}(?:\.\d+)?)(v)?(?:-(air|turbo|flashx|flash|preview))?\b/.exec(modelId);
|
|
147
|
+
if (!match) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const version = parseSemVer(match[1]);
|
|
151
|
+
if (!version) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
family: "glm",
|
|
156
|
+
variant: (match[3] as GlmVariant | undefined) ?? "base",
|
|
157
|
+
vision: match[2] === "v",
|
|
158
|
+
version,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export function isFableOrMythos(kind: AnthropicKind): boolean {
|
|
163
|
+
return kind === "fable" || kind === "mythos";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns true if the parsed Anthropic model is part of the adaptive-thinking
|
|
168
|
+
* Claude generation at or above a specific capability threshold.
|
|
169
|
+
* - Opus has a configurable minimum version floor (e.g. "4.6", "4.7", "4.8").
|
|
170
|
+
* - Sonnet, Fable, and Mythos all require version 5 or higher.
|
|
171
|
+
*/
|
|
172
|
+
export function isAnthropicAdaptiveGenAtLeast(parsed: AnthropicModel, opusMin: "4.6" | "4.7" | "4.8"): boolean {
|
|
173
|
+
if (parsed.kind === "opus") {
|
|
174
|
+
return semverGte(parsed.version, opusMin);
|
|
175
|
+
}
|
|
176
|
+
// Sonnet 5+, Fable 5+, Mythos 5+, and any future gen-5+ models
|
|
177
|
+
return semverGte(parsed.version, "5");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createSemVer(major: number, minor: number, patch = 0): SemVer {
|
|
181
|
+
return { major, minor, patch };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// extend this table if we need anything more than 9.10
|
|
185
|
+
const precomputeTable: Record<string, SemVer> = {};
|
|
186
|
+
for (let major = 0; major <= 9; major++) {
|
|
187
|
+
for (let minor = 0; minor <= 10; minor++) {
|
|
188
|
+
const version = createSemVer(major, minor, 0);
|
|
189
|
+
precomputeTable[`${major}.${minor}`] = version;
|
|
190
|
+
precomputeTable[`${major}-${minor}`] = version;
|
|
191
|
+
}
|
|
192
|
+
precomputeTable[`${major}`] = createSemVer(major, 0, 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseSemVer(version: string): SemVer | null {
|
|
196
|
+
return precomputeTable[version] ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function semverGte(left: SemVer | string, right: SemVer | string): boolean {
|
|
200
|
+
return compareSemVer(left, right) >= 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function semverEqual(left: SemVer | string, right: SemVer | string): boolean {
|
|
204
|
+
return compareSemVer(left, right) === 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function compareSemVer(left: SemVer | string | null, right: SemVer | string | null): number {
|
|
208
|
+
left = typeof left === "string" ? parseSemVer(left) : left;
|
|
209
|
+
right = typeof right === "string" ? parseSemVer(right) : right;
|
|
210
|
+
if (!left || !right) return (left ? 1 : 0) - (right ? 1 : 0);
|
|
211
|
+
|
|
212
|
+
if (left.major !== right.major) {
|
|
213
|
+
return left.major - right.major;
|
|
214
|
+
}
|
|
215
|
+
if (left.minor !== right.minor) {
|
|
216
|
+
return left.minor - right.minor;
|
|
217
|
+
}
|
|
218
|
+
return left.patch - right.patch;
|
|
219
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { modelFamilyToken } from "./family";
|
|
2
|
+
|
|
3
|
+
export type Dialect =
|
|
4
|
+
| "glm"
|
|
5
|
+
| "hermes"
|
|
6
|
+
| "kimi"
|
|
7
|
+
| "xml"
|
|
8
|
+
| "anthropic"
|
|
9
|
+
| "deepseek"
|
|
10
|
+
| "harmony"
|
|
11
|
+
| "qwen3"
|
|
12
|
+
| "gemini"
|
|
13
|
+
| "gemma"
|
|
14
|
+
| "minimax";
|
|
15
|
+
|
|
16
|
+
export const FALLBACK_DIALECT: Dialect = "xml";
|
|
17
|
+
|
|
18
|
+
export function preferredDialect(modelId: string): Dialect {
|
|
19
|
+
switch (modelFamilyToken(modelId)) {
|
|
20
|
+
case "anthropic":
|
|
21
|
+
return "anthropic";
|
|
22
|
+
case "glm":
|
|
23
|
+
return "glm";
|
|
24
|
+
case "gemini":
|
|
25
|
+
return "gemini";
|
|
26
|
+
case "gemma":
|
|
27
|
+
return "gemma";
|
|
28
|
+
case "kimi":
|
|
29
|
+
return "kimi";
|
|
30
|
+
case "qwen":
|
|
31
|
+
return "qwen3";
|
|
32
|
+
case "deepseek":
|
|
33
|
+
return "deepseek";
|
|
34
|
+
case "minimax":
|
|
35
|
+
return "minimax";
|
|
36
|
+
case "openai":
|
|
37
|
+
case "gpt-oss":
|
|
38
|
+
return "harmony";
|
|
39
|
+
default:
|
|
40
|
+
return FALLBACK_DIALECT;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-family id predicates: the shared vocabulary for "is this id a member
|
|
3
|
+
* of family X" checks that gate wire-level behavior across hosts (a Kimi or
|
|
4
|
+
* DeepSeek model keeps its quirks no matter which OpenAI-compatible proxy
|
|
5
|
+
* serves it). Looser per-feature heuristics (e.g. stream-markup healing)
|
|
6
|
+
* deliberately keep their own patterns — only provably-shared matchers live
|
|
7
|
+
* here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
bareModelId,
|
|
12
|
+
isAnthropicAdaptiveGenAtLeast,
|
|
13
|
+
isFableOrMythos,
|
|
14
|
+
parseAnthropicModel,
|
|
15
|
+
parseGlmModel,
|
|
16
|
+
parseKnownModel,
|
|
17
|
+
parseOpenAIModel,
|
|
18
|
+
semverGte,
|
|
19
|
+
} from "./classify";
|
|
20
|
+
|
|
21
|
+
/** Bounded process-lifetime cache memo helper. */
|
|
22
|
+
function memo<T>(fn: (modelId: string) => T): (modelId: string) => T {
|
|
23
|
+
const cache = new Map<string, T>();
|
|
24
|
+
return (modelId: string) => {
|
|
25
|
+
if (cache.has(modelId)) {
|
|
26
|
+
return cache.get(modelId) as T;
|
|
27
|
+
}
|
|
28
|
+
const result = fn(modelId);
|
|
29
|
+
cache.set(modelId, result);
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
|
|
35
|
+
export const isKimiModelId = memo((modelId: string): boolean => {
|
|
36
|
+
return modelId.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(modelId);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
|
|
40
|
+
export const isKimiK26ModelId = memo((modelId: string): boolean => {
|
|
41
|
+
return /(^|\/)kimi-k2(?:\.6|p6)(?:[-:]|$)/i.test(modelId);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Kimi K3 in any namespace form (`kimi-k3`, `kimi-k3.1`, `kimi-k3-turbo`,
|
|
46
|
+
* `moonshotai/kimi-k3`). K3 always reasons and drives thinking via OpenAI-style
|
|
47
|
+
* `reasoning_effort: "max"`, not the K2.x binary `thinking: { type }` block —
|
|
48
|
+
* see the moonshot discovery mapper and `buildOpenAICompat`.
|
|
49
|
+
*/
|
|
50
|
+
export const isKimiK3ModelId = memo((modelId: string): boolean => {
|
|
51
|
+
return /(^|\/)kimi-k3(?:\.\d+)?(?:[-.:_]|$)/i.test(modelId);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Claude ids in any namespace form: bare (`claude-*`), path-namespaced
|
|
56
|
+
* (`anthropic/claude.x`), or dot-prefixed (`us.anthropic.claude-…`,
|
|
57
|
+
* `global.anthropic.claude-…`, `au.anthropic.claude-…` — Bedrock cross-region
|
|
58
|
+
* inference profiles). Necessary because {@link parseAnthropicModel} only
|
|
59
|
+
* classifies kinds enumerated in its regex, so any dotted profile whose kind
|
|
60
|
+
* (e.g. `haiku`) is not enumerated would otherwise slip past this fallback.
|
|
61
|
+
*/
|
|
62
|
+
export const isClaudeModelId = memo((modelId: string): boolean => {
|
|
63
|
+
return /(^|[/.])claude[-.]/i.test(modelId);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
|
|
67
|
+
export const isAnthropicNamespacedModelId = memo((modelId: string): boolean => {
|
|
68
|
+
return /(^|\/)anthropic\//i.test(modelId);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
|
|
72
|
+
export const isQwenModelId = memo((modelId: string): boolean => {
|
|
73
|
+
return modelId.toLowerCase().includes("qwen");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
|
|
77
|
+
export const isGemmaModelId = memo((modelId: string): boolean => {
|
|
78
|
+
return /(^|\/)gemma[-.]?\d/i.test(modelId);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
|
|
82
|
+
export const isDeepseekModelIdOrName = memo((value: string): boolean => {
|
|
83
|
+
return value.toLowerCase().includes("deepseek");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
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.
|
|
91
|
+
*/
|
|
92
|
+
export const isDeepseekV4FlashModelId = memo((modelId: string): boolean => {
|
|
93
|
+
return bareModelId(modelId).toLowerCase().includes("deepseek-v4-flash");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** Xiaomi MiMo family by id or display name. */
|
|
97
|
+
export const isMimoModelIdOrName = memo((value: string): boolean => {
|
|
98
|
+
return value.toLowerCase().includes("mimo");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3", "grok-4.5"] as const;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
|
|
105
|
+
* (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
|
|
106
|
+
* param, so callers must omit reasoning effort for them.
|
|
107
|
+
*/
|
|
108
|
+
export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
|
|
109
|
+
const bare = bareModelId(modelId).trim().toLowerCase();
|
|
110
|
+
if (!bare) return false;
|
|
111
|
+
return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
|
|
116
|
+
* `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
|
|
117
|
+
* and short `minimax/m2-…` ids on aggregator hosts). Underlying model accepts
|
|
118
|
+
* only `low|medium|high` for `reasoning_effort` and 400s on `minimal`,
|
|
119
|
+
* `xhigh`, or `none` — so hosts whose default effort map otherwise lowers
|
|
120
|
+
* `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
|
|
121
|
+
* clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
|
|
122
|
+
*/
|
|
123
|
+
export const isMinimaxM2FamilyModelId = memo((modelId: string): boolean => {
|
|
124
|
+
const lower = modelId.toLowerCase();
|
|
125
|
+
if (!lower.includes("minimax")) return false;
|
|
126
|
+
// Boundary-delimited `m2` token followed by zero or more digits (dotless
|
|
127
|
+
// variants like `m21`/`m25`/`m27`) and an optional dotted minor version.
|
|
128
|
+
return /(?:^|[/.-])m2\d*(?:[.-]\d+)?(?:[-.:_]|$)/i.test(lower);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
|
|
132
|
+
export const isMinimaxM3FamilyModelId = memo((modelId: string): boolean => {
|
|
133
|
+
const lower = modelId.toLowerCase();
|
|
134
|
+
if (!lower.includes("minimax")) return false;
|
|
135
|
+
return /(?:^|[/._-])(?:minimax[/._-])?m3(?:[-.:_]|$)/i.test(lower);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
|
|
140
|
+
* `vendor/gpt-oss-…`). The Harmony reasoning format only accepts
|
|
141
|
+
* `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
|
|
142
|
+
* and `none`.
|
|
143
|
+
*/
|
|
144
|
+
export const isOpenAIGptOssModelId = memo((modelId: string): boolean => {
|
|
145
|
+
return /(^|\/)gpt-oss[-:]/i.test(modelId);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** OpenAI model ids (gpt-*, chatgpt-*, o1/o3/o4 SKUs, codex-*, or openai/*). */
|
|
149
|
+
export const isOpenAIModelId = memo((modelId: string): boolean => {
|
|
150
|
+
return (
|
|
151
|
+
/(^|\/)(?:gpt|chatgpt|codex)[-.]/i.test(modelId) ||
|
|
152
|
+
/(^|\/)o[134](?:[-.]|$)/i.test(modelId) ||
|
|
153
|
+
modelId.toLowerCase().includes("openai/")
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/** OpenAI models at or above the gpt-5.4 wire generation, keyed off the parsed version. */
|
|
158
|
+
const isOpenAIWireGen54Plus = memo((modelId: string): boolean => {
|
|
159
|
+
const parsed = parseOpenAIModel(bareModelId(modelId));
|
|
160
|
+
if (!parsed) return false;
|
|
161
|
+
return semverGte(parsed.version, "5.4");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* OpenAI Codex models that honor `reasoning.context: "all_turns"` (full
|
|
166
|
+
* cross-turn reasoning replay). The `reasoning.context` field itself exists for
|
|
167
|
+
* the whole gpt-5/o-series family, but the `all_turns` value is only accepted
|
|
168
|
+
* from gpt-5.4 onward; earlier ids (`gpt-5.1-codex`, `gpt-5.3-codex`, and
|
|
169
|
+
* `gpt-5.3-codex-spark`) reject it with
|
|
170
|
+
* `Unsupported value: 'all_turns' is not supported with this model`. Version
|
|
171
|
+
* floor (not an allowlist) so 5.6/6.x inherit support automatically. Callers
|
|
172
|
+
* fall back to omitting `context`, letting the server default to `current_turn`.
|
|
173
|
+
*/
|
|
174
|
+
export const supportsAllTurnsReasoningContext = isOpenAIWireGen54Plus;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* OpenAI Codex models that accept `reasoning.summary`. Shares the gpt-5.4 wire
|
|
178
|
+
* floor with {@link supportsAllTurnsReasoningContext}: earlier Codex ids
|
|
179
|
+
* (`gpt-5.1-codex`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`) reject the field
|
|
180
|
+
* with `Unsupported parameter: 'reasoning.summary' is not supported with this
|
|
181
|
+
* model`. Callers omit `summary` for unsupported ids, letting the server skip
|
|
182
|
+
* the human-readable summary stream.
|
|
183
|
+
*/
|
|
184
|
+
export const supportsCodexReasoningSummary = isOpenAIWireGen54Plus;
|
|
185
|
+
|
|
186
|
+
/** OpenAI proprietary reasoning families keyed off the parsed gpt version (gpt-5+). */
|
|
187
|
+
const isOpenAIWireGen5Plus = memo((modelId: string): boolean => {
|
|
188
|
+
const parsed = parseOpenAIModel(bareModelId(modelId));
|
|
189
|
+
if (!parsed) return false;
|
|
190
|
+
return semverGte(parsed.version, "5");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/** o-series reasoning ids (`o1`, `o1-pro`, `o3`, `o3-mini`, `o4-mini`, `openai/o3`, …). */
|
|
194
|
+
const O_SERIES_REASONING_RE = /(^|\/)o[134](?:[-.]|$)/i;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* OpenAI proprietary models whose serving path rejects explicit sampling
|
|
198
|
+
* parameters (`temperature`, `top_p`, `top_k`, …) with
|
|
199
|
+
* `400 Unsupported parameter: 'temperature' is not supported with this model`.
|
|
200
|
+
* Covers the o-series and the entire gpt-5+ generation — base, `mini`, `nano`,
|
|
201
|
+
* `codex*`, the `luna`/`sol`/`terra` SKUs, and the `-chat-latest` variants,
|
|
202
|
+
* since even the non-reasoning gpt-5 chat models reject sampling params (see
|
|
203
|
+
* litellm#13781). Holds regardless of which OpenAI-serving host proxies the
|
|
204
|
+
* model (official, Azure, GitHub Copilot). Version floor (not an allowlist) so
|
|
205
|
+
* 6.x inherits automatically. Issue #5606.
|
|
206
|
+
*/
|
|
207
|
+
export const isOpenAISamplingRestrictedModelId = memo((modelId: string): boolean => {
|
|
208
|
+
const bare = bareModelId(modelId);
|
|
209
|
+
return isOpenAIWireGen5Plus(modelId) || O_SERIES_REASONING_RE.test(bare);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
|
|
214
|
+
* `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
|
|
215
|
+
* `-flash`/`-flashx`/`-preview` variants, and pre-4.5 ids. Matching the family
|
|
216
|
+
* keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
|
|
217
|
+
* allowlist.
|
|
218
|
+
*/
|
|
219
|
+
export const isReasoningGlmModelId = memo((modelId: string): boolean => {
|
|
220
|
+
const glm = parseGlmModel(bareModelId(modelId));
|
|
221
|
+
if (!glm || glm.vision) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return semverGte(glm.version, "4.5");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
|
|
231
|
+
export const isGlm52ReasoningEffortModelId = memo((modelId: string): boolean => {
|
|
232
|
+
const glm = parseGlmModel(bareModelId(modelId));
|
|
233
|
+
if (!glm || glm.vision) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return semverGte(glm.version, "5.2");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
|
|
243
|
+
export const isGlmVisionModelId = memo((modelId: string): boolean => {
|
|
244
|
+
return parseGlmModel(bareModelId(modelId))?.vision === true;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Coarse vendor-lineage token for "are two models the same family?" checks
|
|
249
|
+
* (e.g. picking a cross-family reviewer). All Claude point releases share a token,
|
|
250
|
+
* Claude and GPT differ; namespace prefixes and aggregator mirrors fold onto the
|
|
251
|
+
* lineage via {@link parseKnownModel}'s `bareModelId` normalization. Opaque and
|
|
252
|
+
* comparison-only — not a stable key to persist, since the vocabulary tracks new
|
|
253
|
+
* releases. Returns `""` for ids it cannot classify; callers fall back to the provider.
|
|
254
|
+
*
|
|
255
|
+
* Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
|
|
256
|
+
* collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
|
|
257
|
+
*/
|
|
258
|
+
export const modelFamilyToken = memo((modelId: string): string => {
|
|
259
|
+
const parsed = parseKnownModel(modelId);
|
|
260
|
+
if (parsed.family !== "unknown") return parsed.family;
|
|
261
|
+
if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
|
|
262
|
+
if (isOpenAIModelId(modelId)) return "openai";
|
|
263
|
+
if (isKimiModelId(modelId)) return "kimi";
|
|
264
|
+
if (isQwenModelId(modelId)) return "qwen";
|
|
265
|
+
if (isMinimaxM2FamilyModelId(modelId) || isMinimaxM3FamilyModelId(modelId)) return "minimax";
|
|
266
|
+
if (isOpenAIGptOssModelId(modelId)) return "gpt-oss";
|
|
267
|
+
if (isDeepseekModelIdOrName(modelId)) return "deepseek";
|
|
268
|
+
if (isMimoModelIdOrName(modelId)) return "mimo";
|
|
269
|
+
if (isGemmaModelId(modelId)) return "gemma";
|
|
270
|
+
if (parseGlmModel(bareModelId(modelId))) return "glm";
|
|
271
|
+
return "";
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* True for Claude generations that support extended thinking: Sonnet/Opus 3.7+,
|
|
276
|
+
* every 4.x/5+ Opus/Sonnet, and the Fable/Mythos generation. Pre-thinking
|
|
277
|
+
* models (Claude 3.5 and older) are excluded so no thinking effort dial is
|
|
278
|
+
* fabricated for a model that rejects thinking parameters. Classifier-based, so
|
|
279
|
+
* dotted and dashed version forms both match; ids the classifier does not parse
|
|
280
|
+
* (e.g. Haiku, bare dated ids) return false.
|
|
281
|
+
*/
|
|
282
|
+
export const anthropicModelSupportsThinking = memo((modelId: string): boolean => {
|
|
283
|
+
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
284
|
+
return parsed !== null && semverGte(parsed.version, "3.7");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
|
|
289
|
+
* Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
|
|
290
|
+
* models (Opus 4.6, Sonnet 4.6) reject the field. Classifier-based, so dotted
|
|
291
|
+
* and dashed version forms both match while bare dated ids
|
|
292
|
+
* (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
|
|
293
|
+
*/
|
|
294
|
+
export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
|
|
295
|
+
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
296
|
+
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Returns true for Anthropic models with Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+
|
|
301
|
+
* API restrictions:
|
|
302
|
+
* - Sampling parameters (temperature/top_p/top_k) return 400 error
|
|
303
|
+
* - Thinking content is omitted by default (needs display: "summarized")
|
|
304
|
+
*/
|
|
305
|
+
export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
|
|
306
|
+
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
307
|
+
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Mid-conversation `role: "system"` messages (system instructions appended at
|
|
312
|
+
* non-first positions in the `messages` array) are supported starting with
|
|
313
|
+
* Claude Opus 4.8+, Sonnet 5+, and the Claude Fable/Mythos 5 generation.
|
|
314
|
+
* Earlier Claude models reject the role.
|
|
315
|
+
* @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
|
|
316
|
+
*/
|
|
317
|
+
export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
|
|
318
|
+
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
319
|
+
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.8");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
|
|
323
|
+
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
324
|
+
return parsed !== null && isFableOrMythos(parsed.kind);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/** Thinking-variant token location inside a model id. */
|
|
328
|
+
export interface ThinkingVariantToken {
|
|
329
|
+
index: number;
|
|
330
|
+
length: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const THINKING_VARIANT_TOKEN_RE = /-(?:thinking|reasoner|reasoning)(?=$|[^a-z0-9])/gi;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Locates the first thinking-variant token (`-thinking`, `-reasoner`,
|
|
337
|
+
* `-reasoning`; trailing or infix) in a model id. The token ends at the id
|
|
338
|
+
* end or any non-alphanumeric boundary, and negated forms (`non-thinking`,
|
|
339
|
+
* `no-thinking`) never match — those name the NON-thinking SKU.
|
|
340
|
+
*/
|
|
341
|
+
export function findThinkingVariantToken(modelId: string): ThinkingVariantToken | undefined {
|
|
342
|
+
THINKING_VARIANT_TOKEN_RE.lastIndex = 0;
|
|
343
|
+
let match = THINKING_VARIANT_TOKEN_RE.exec(modelId);
|
|
344
|
+
while (match !== null) {
|
|
345
|
+
const preceding = /([a-z0-9]+)$/i.exec(modelId.slice(0, match.index))?.[1]?.toLowerCase();
|
|
346
|
+
if (preceding !== "non" && preceding !== "no") {
|
|
347
|
+
return { index: match.index, length: match[0].length };
|
|
348
|
+
}
|
|
349
|
+
match = THINKING_VARIANT_TOKEN_RE.exec(modelId);
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Removes the located thinking-variant token: `kimi-k2-thinking` → `kimi-k2`,
|
|
356
|
+
* `mimo-v2-flash-thinking-original` → `mimo-v2-flash-original`,
|
|
357
|
+
* `grok-4.1-fast-reasoning` → `grok-4.1-fast`. Returns `undefined` when no
|
|
358
|
+
* token exists or nothing would remain. Callers MUST verify the result names
|
|
359
|
+
* a live model.
|
|
360
|
+
*/
|
|
361
|
+
export const stripThinkingVariantToken = memo((modelId: string): string | undefined => {
|
|
362
|
+
const token = findThinkingVariantToken(modelId);
|
|
363
|
+
if (!token) return undefined;
|
|
364
|
+
const stripped = modelId.slice(0, token.index) + modelId.slice(token.index + token.length);
|
|
365
|
+
return stripped.length > 0 ? stripped : undefined;
|
|
366
|
+
});
|