@oh-my-pi/pi-catalog 17.3.7 → 17.4.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 +36 -0
- package/dist/types/build.d.ts +5 -0
- package/dist/types/discovery/cursor-proto.d.ts +5362 -0
- package/dist/types/discovery/devin-proto.d.ts +1549 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/protobuf.d.ts +102 -0
- package/dist/types/identity/family.d.ts +23 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model-thinking.d.ts +6 -2
- package/dist/types/model-tokenizer.d.ts +8 -0
- package/dist/types/provider-models/descriptors.d.ts +1 -0
- package/dist/types/types.d.ts +27 -1
- package/dist/types/variant-collapse.d.ts +2 -0
- package/package.json +5 -6
- package/src/build.ts +7 -0
- package/src/compat/openai.ts +27 -1
- package/src/discovery/codex.ts +118 -24
- package/src/discovery/cursor-proto.ts +8322 -0
- package/src/discovery/cursor.ts +16 -3
- package/src/discovery/devin-proto.ts +2037 -0
- package/src/discovery/devin.ts +4 -3
- package/src/discovery/index.ts +1 -0
- package/src/discovery/protobuf.ts +1073 -0
- package/src/hosts.ts +22 -1
- package/src/identity/family.ts +42 -1
- package/src/index.ts +1 -0
- package/src/model-cache.ts +67 -6
- package/src/model-thinking.ts +38 -6
- package/src/model-tokenizer.ts +75 -0
- package/src/models.json +185055 -2118
- package/src/models.ts +6 -5
- package/src/provider-models/bundled-references.ts +12 -2
- package/src/provider-models/cache-provider-id.ts +3 -1
- package/src/provider-models/descriptors.ts +1 -0
- package/src/provider-models/openai-compat.ts +194 -44
- package/src/types.ts +36 -0
- package/src/variant-collapse.ts +67 -34
- package/dist/types/discovery/cursor-gen/agent_pb.d.ts +0 -16769
- package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +0 -1715
- package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +0 -693
- package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +0 -9158
- package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +0 -52
- package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +0 -276
- package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +0 -78
- package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +0 -999
- package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +0 -1704
- package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +0 -656
- package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +0 -15014
- package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +0 -607
- package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +0 -11830
- package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +0 -248
- package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +0 -1747
- package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +0 -509
- package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +0 -9048
- package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +0 -1760
- package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +0 -286
- package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +0 -405
- package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +0 -582
- package/src/discovery/cursor-gen/agent_pb.ts +0 -19513
- package/src/discovery/devin-gen/buf/validate/validate_pb.ts +0 -1862
- package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +0 -871
- package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +0 -11083
- package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +0 -71
- package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +0 -348
- package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +0 -103
- package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +0 -1198
- package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +0 -2063
- package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +0 -810
- package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +0 -18354
- package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +0 -732
- package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +0 -14277
- package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +0 -312
- package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +0 -2106
- package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +0 -623
- package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +0 -10918
- package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +0 -2125
- package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +0 -361
- package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +0 -450
- package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +0 -712
package/src/hosts.ts
CHANGED
|
@@ -72,13 +72,34 @@ export const KNOWN_HOSTS = {
|
|
|
72
72
|
|
|
73
73
|
export type KnownHost = keyof typeof KNOWN_HOSTS;
|
|
74
74
|
|
|
75
|
+
// Host checks fan out across every compatibility field for a model. Bound the
|
|
76
|
+
// cache because custom providers may contribute arbitrary endpoints at runtime.
|
|
77
|
+
const MAX_URL_HOST_MATCHES = 512;
|
|
78
|
+
const urlHostMatches = new Map<string, Map<KnownHost, boolean>>();
|
|
79
|
+
|
|
80
|
+
function getUrlHostMatches(baseUrl: string): Map<KnownHost, boolean> {
|
|
81
|
+
let matches = urlHostMatches.get(baseUrl);
|
|
82
|
+
if (matches !== undefined) return matches;
|
|
83
|
+
if (urlHostMatches.size === MAX_URL_HOST_MATCHES) urlHostMatches.clear();
|
|
84
|
+
matches = new Map<KnownHost, boolean>();
|
|
85
|
+
urlHostMatches.set(baseUrl, matches);
|
|
86
|
+
return matches;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/** URL-only host check (for call sites that have no provider id, e.g. raw env config). */
|
|
76
90
|
export function hostMatchesUrl(baseUrl: string | undefined, host: KnownHost): boolean {
|
|
77
91
|
if (!baseUrl) return false;
|
|
92
|
+
const matches = getUrlHostMatches(baseUrl);
|
|
93
|
+
const cached = matches.get(host);
|
|
94
|
+
if (cached !== undefined) return cached;
|
|
78
95
|
const spec: HostClassSpec = KNOWN_HOSTS[host];
|
|
79
96
|
for (const marker of spec.urlMarkers) {
|
|
80
|
-
if (includesAsciiCaseInsensitive(baseUrl, marker))
|
|
97
|
+
if (includesAsciiCaseInsensitive(baseUrl, marker)) {
|
|
98
|
+
matches.set(host, true);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
81
101
|
}
|
|
102
|
+
matches.set(host, false);
|
|
82
103
|
return false;
|
|
83
104
|
}
|
|
84
105
|
|
package/src/identity/family.ts
CHANGED
|
@@ -73,6 +73,25 @@ export const isQwenModelId = memo((modelId: string): boolean => {
|
|
|
73
73
|
return modelId.toLowerCase().includes("qwen");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Open-weight Qwen 3.8+ releases (`qwen3.8-27b`, `qwen3.8-2.4t-a95b`, GGUF
|
|
78
|
+
* names like `Qwen3.8-27B-UD-Q6_K_XL`) whose chat template steers thinking
|
|
79
|
+
* depth through a `reasoning_effort` template kwarg (`low`/`medium`/`xhigh`,
|
|
80
|
+
* template default `xhigh`; thinking itself cannot be disabled). Compared
|
|
81
|
+
* component-wise so `qwen3.10` sorts after `qwen3.8`. API-only `-max` SKUs are
|
|
82
|
+
* excluded — Dashscope drives them through OpenAI-style `reasoning_effort`
|
|
83
|
+
* with curated compat. The trailing guard rejects parameter-count lookalikes
|
|
84
|
+
* (`qwen-3.8b`) without breaking `qwen3.8-27b`.
|
|
85
|
+
*/
|
|
86
|
+
export const isQwen38PlusTemplateEffortModelId = memo((modelId: string): boolean => {
|
|
87
|
+
const match = /qwen[-_ ]?(\d+)\.(\d+)(?![\dbB])/i.exec(modelId);
|
|
88
|
+
if (!match) return false;
|
|
89
|
+
const major = Number.parseInt(match[1], 10);
|
|
90
|
+
const minor = Number.parseInt(match[2], 10);
|
|
91
|
+
if (major < 3 || (major === 3 && minor < 8)) return false;
|
|
92
|
+
return !/^-max(?:$|[-.:])/i.test(modelId.slice(match.index + match[0].length));
|
|
93
|
+
});
|
|
94
|
+
|
|
76
95
|
/** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
|
|
77
96
|
export const isGemmaModelId = memo((modelId: string): boolean => {
|
|
78
97
|
return /(^|\/)gemma[-.]?\d/i.test(modelId);
|
|
@@ -100,6 +119,11 @@ export const isMimoModelIdOrName = memo((value: string): boolean => {
|
|
|
100
119
|
return value.toLowerCase().includes("mimo");
|
|
101
120
|
});
|
|
102
121
|
|
|
122
|
+
/** StepFun Step 3.7 Flash SKU in any namespace form (`kilo/stepfun/step-3.7-flash:free`). */
|
|
123
|
+
export const isStep37FlashModelId = memo((modelId: string): boolean => {
|
|
124
|
+
return modelId.toLowerCase().includes("step-3.7-flash");
|
|
125
|
+
});
|
|
126
|
+
|
|
103
127
|
/** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
|
|
104
128
|
export const isGeminiModelId = memo((modelId: string): boolean => {
|
|
105
129
|
return /(^|\/)gemini[-.]?/i.test(modelId);
|
|
@@ -121,7 +145,8 @@ const GROK_EFFORT_CAPABLE_PREFIXES = [
|
|
|
121
145
|
/**
|
|
122
146
|
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
|
|
123
147
|
* (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
|
|
124
|
-
* param, so callers must omit reasoning effort for them.
|
|
148
|
+
* param, so callers must omit reasoning effort for them. `grok-4.6` accepts
|
|
149
|
+
* `low`/`medium`/`high`/`xhigh` and 400s on `max`.
|
|
125
150
|
*/
|
|
126
151
|
export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
|
|
127
152
|
const bare = bareModelId(modelId).trim().toLowerCase();
|
|
@@ -379,6 +404,22 @@ export const supportsMidConversationSystemMessages = memo((modelId: string): boo
|
|
|
379
404
|
return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.8");
|
|
380
405
|
});
|
|
381
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Models that reliably follow the hashline line-anchored edit dialect
|
|
409
|
+
* (`[path#TAG]` headers plus 1-indexed anchors). Kimi, MiMo, DeepSeek V4
|
|
410
|
+
* Flash, and Step 3.7 Flash miscount anchors or drop the tag header often
|
|
411
|
+
* enough that hosts fall back to a literal search-replace edit format for
|
|
412
|
+
* them.
|
|
413
|
+
*/
|
|
414
|
+
export const supportsHashlineEdits = memo((modelId: string): boolean => {
|
|
415
|
+
return !(
|
|
416
|
+
isKimiModelId(modelId) ||
|
|
417
|
+
isMimoModelIdOrName(modelId) ||
|
|
418
|
+
isDeepseekV4FlashModelId(modelId) ||
|
|
419
|
+
isStep37FlashModelId(modelId)
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
382
423
|
export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
|
|
383
424
|
const parsed = parseAnthropicModel(bareModelId(modelId));
|
|
384
425
|
return parsed !== null && isFableOrMythos(parsed.kind);
|
package/src/index.ts
CHANGED
package/src/model-cache.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Replaces per-provider JSON files with a single cache.db.
|
|
4
4
|
*/
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
|
-
import {
|
|
6
|
+
import { renameSync } from "node:fs";
|
|
7
|
+
import { getModelDbPath, isEnoent, isSqliteCorruptionError, logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import type { Api, Model, ModelSpec } from "./types";
|
|
8
9
|
|
|
9
10
|
// Rows persist ModelSpec JSON (sparse `compat`, never the resolved record);
|
|
@@ -91,13 +92,14 @@ function openDb(resolvedPath: string): Database {
|
|
|
91
92
|
return db;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function getSharedDb(): Database {
|
|
95
|
-
const resolvedPath = getModelDbPath();
|
|
95
|
+
function getSharedDb(resolvedPath: string): Database {
|
|
96
96
|
if (sharedDb && sharedDbPath === resolvedPath) {
|
|
97
97
|
return sharedDb;
|
|
98
98
|
}
|
|
99
99
|
if (sharedDb) {
|
|
100
100
|
sharedDb.close();
|
|
101
|
+
sharedDb = null;
|
|
102
|
+
sharedDbPath = null;
|
|
101
103
|
}
|
|
102
104
|
const db = openDb(resolvedPath);
|
|
103
105
|
sharedDb = db;
|
|
@@ -105,9 +107,9 @@ function getSharedDb(): Database {
|
|
|
105
107
|
return db;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
function
|
|
109
|
-
if (
|
|
110
|
-
const db = openDb(
|
|
110
|
+
function runModelCacheDb<T>(resolvedPath: string, shared: boolean, useDb: (db: Database) => T): T {
|
|
111
|
+
if (shared) return useDb(getSharedDb(resolvedPath));
|
|
112
|
+
const db = openDb(resolvedPath);
|
|
111
113
|
try {
|
|
112
114
|
return useDb(db);
|
|
113
115
|
} finally {
|
|
@@ -115,6 +117,65 @@ function withModelCacheDb<T>(dbPath: string | undefined, useDb: (db: Database) =
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// Paths already reported corrupt this process: the first unrecoverable failure
|
|
121
|
+
// is logged at `error`, later heals at `debug`, so a dying disk cannot spam.
|
|
122
|
+
const reportedCorruptPaths = new Set<string>();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Move a physically corrupt `models.db` (plus its `-wal`/`-shm` sidecars) aside
|
|
126
|
+
* so {@link openDb} can recreate a fresh cache at the original path. Renames are
|
|
127
|
+
* best-effort: a vanished sidecar (already healed by a peer process) is fine,
|
|
128
|
+
* and any other rename failure is left for {@link openDb} to surface.
|
|
129
|
+
*/
|
|
130
|
+
function quarantineCorruptModelCache(resolvedPath: string): void {
|
|
131
|
+
const stamp = Date.now();
|
|
132
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
133
|
+
try {
|
|
134
|
+
renameSync(`${resolvedPath}${suffix}`, `${resolvedPath}.corrupt-${stamp}${suffix}`);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (!isEnoent(err)) {
|
|
137
|
+
logger.debug("model cache: could not quarantine corrupt file", { path: `${resolvedPath}${suffix}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Recover from unrecoverable `models.db` corruption: drop the cached handle,
|
|
145
|
+
* quarantine the broken files, and let the next open recreate the cache. A
|
|
146
|
+
* corrupt cache would otherwise be re-queried on every read/write forever,
|
|
147
|
+
* permanently masking a successful live catalog (issue #8867). Only
|
|
148
|
+
* {@link isSqliteCorruptionError} codes reach here; BUSY/permission errors keep
|
|
149
|
+
* their existing best-effort paths.
|
|
150
|
+
*/
|
|
151
|
+
function healCorruptModelCache(resolvedPath: string, shared: boolean, err: unknown): void {
|
|
152
|
+
if (shared && sharedDb) {
|
|
153
|
+
sharedDb.close();
|
|
154
|
+
sharedDb = null;
|
|
155
|
+
sharedDbPath = null;
|
|
156
|
+
}
|
|
157
|
+
quarantineCorruptModelCache(resolvedPath);
|
|
158
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
159
|
+
if (reportedCorruptPaths.has(resolvedPath)) {
|
|
160
|
+
logger.debug("model cache: re-healed corrupt database", { path: resolvedPath, code });
|
|
161
|
+
} else {
|
|
162
|
+
reportedCorruptPaths.add(resolvedPath);
|
|
163
|
+
logger.error("model cache corrupt; quarantined and recreated a fresh cache", { path: resolvedPath, code });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function withModelCacheDb<T>(dbPath: string | undefined, useDb: (db: Database) => T): T {
|
|
168
|
+
const resolvedPath = dbPath ?? getModelDbPath();
|
|
169
|
+
const shared = dbPath === undefined;
|
|
170
|
+
try {
|
|
171
|
+
return runModelCacheDb(resolvedPath, shared, useDb);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (!isSqliteCorruptionError(err)) throw err;
|
|
174
|
+
healCorruptModelCache(resolvedPath, shared, err);
|
|
175
|
+
return runModelCacheDb(resolvedPath, shared, useDb);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
118
179
|
function migrateCacheSchema(db: Database): void {
|
|
119
180
|
const stmt = db.prepare("PRAGMA table_info(model_cache)");
|
|
120
181
|
try {
|
package/src/model-thinking.ts
CHANGED
|
@@ -71,6 +71,11 @@ const LOW_HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Hi
|
|
|
71
71
|
const HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.Max];
|
|
72
72
|
/** OpenRouter's DeepSeek route accepts only `high`. */
|
|
73
73
|
const HIGH_ONLY_REASONING_EFFORTS: readonly Effort[] = [Effort.High];
|
|
74
|
+
/**
|
|
75
|
+
* Qwen 3.8+ open-weight chat template: prompt-steered `reasoning_effort`
|
|
76
|
+
* kwarg with exactly three wire tiers (template default is `xhigh`).
|
|
77
|
+
*/
|
|
78
|
+
const QWEN38_TEMPLATE_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.XHigh];
|
|
74
79
|
/**
|
|
75
80
|
* Five wire tiers with a `low` floor: GPT-5.6+, Anthropic adaptive models
|
|
76
81
|
* with the real xhigh tier (Opus 4.7+, Sonnet 5+, Fable/Mythos 5), and the
|
|
@@ -179,7 +184,9 @@ function fillThinkingWireDefaults<TApi extends Api>(
|
|
|
179
184
|
thinking.supportsDisplay === undefined &&
|
|
180
185
|
(spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") &&
|
|
181
186
|
supportsAdaptiveThinkingDisplay(spec.id);
|
|
182
|
-
const needsRequiresEffort =
|
|
187
|
+
const needsRequiresEffort =
|
|
188
|
+
thinking.requiresEffort === undefined &&
|
|
189
|
+
(impliesMandatoryReasoning(parsed, spec.id) || isQwenTemplateReasoningEffortCompat(compat));
|
|
183
190
|
const needsDefaultLevel =
|
|
184
191
|
thinking.defaultLevel === undefined && (isKimiK3ModelId(spec.id) || isGlm53ReasoningEffortModelId(spec.id));
|
|
185
192
|
if (!effortsChanged && !shouldReplaceEffortMap && !needsDisplay && !needsRequiresEffort && !needsDefaultLevel) {
|
|
@@ -232,7 +239,7 @@ export function deriveThinking<TApi extends Api>(spec: ModelSpec<TApi>, compat:
|
|
|
232
239
|
) {
|
|
233
240
|
config.supportsDisplay = true;
|
|
234
241
|
}
|
|
235
|
-
if (impliesMandatoryReasoning(parsed, spec.id)) {
|
|
242
|
+
if (impliesMandatoryReasoning(parsed, spec.id) || isQwenTemplateReasoningEffortCompat(compat)) {
|
|
236
243
|
config.requiresEffort = true;
|
|
237
244
|
}
|
|
238
245
|
return config;
|
|
@@ -376,6 +383,13 @@ function getModelDefinedEfforts<TApi extends Api>(
|
|
|
376
383
|
if (spec.provider === "ollama") {
|
|
377
384
|
return OLLAMA_REASONING_EFFORTS;
|
|
378
385
|
}
|
|
386
|
+
// Qwen 3.8+ served through a local llama.cpp-style backend: the chat
|
|
387
|
+
// template's prompt-steered `reasoning_effort` kwarg accepts exactly
|
|
388
|
+
// low/medium/xhigh (and thinking cannot be turned off — the official 3.8
|
|
389
|
+
// template raises on `enable_thinking: false`, hence requiresEffort).
|
|
390
|
+
if (isOpenAICompatReasoningApi(spec.api) && isQwenTemplateReasoningEffortCompat(compat)) {
|
|
391
|
+
return QWEN38_TEMPLATE_REASONING_EFFORTS;
|
|
392
|
+
}
|
|
379
393
|
if (
|
|
380
394
|
(isOpenAICompatReasoningApi(spec.api) || (spec.api === "ollama-chat" && spec.provider === "ollama-cloud")) &&
|
|
381
395
|
isDeepseekReasoningModel(spec)
|
|
@@ -483,6 +497,12 @@ function isOpenRouterThinkingFormat(compat: CompatOf<Api>): boolean {
|
|
|
483
497
|
function isZaiThinkingFormat(compat: CompatOf<Api>): boolean {
|
|
484
498
|
return compat !== undefined && "thinkingFormat" in compat && compat.thinkingFormat === "zai";
|
|
485
499
|
}
|
|
500
|
+
/** Resolved-compat gate for the Qwen 3.8+ local template `reasoning_effort` dialect. */
|
|
501
|
+
function isQwenTemplateReasoningEffortCompat(compat: CompatOf<Api>): boolean {
|
|
502
|
+
return (
|
|
503
|
+
compat !== undefined && "qwenTemplateReasoningEffort" in compat && compat.qwenTemplateReasoningEffort === true
|
|
504
|
+
);
|
|
505
|
+
}
|
|
486
506
|
|
|
487
507
|
function inferDetectedEffortMap<TApi extends Api>(
|
|
488
508
|
spec: ModelSpec<TApi>,
|
|
@@ -796,11 +816,23 @@ export function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>,
|
|
|
796
816
|
return effort;
|
|
797
817
|
}
|
|
798
818
|
|
|
799
|
-
/** Maps a normalized thinking effort to Google's `thinkingLevel` enum values.
|
|
800
|
-
|
|
819
|
+
/** Maps a normalized thinking effort to Google's `thinkingLevel` enum values.
|
|
820
|
+
* When a collapsed family routes `minimal` onto the same wire id as `low`
|
|
821
|
+
* (Antigravity Gemini 3.6/3.7 Flash), emit `LOW` — Cloud Code Assist rejects
|
|
822
|
+
* `MINIMAL` on those `-low` SKUs.
|
|
823
|
+
*/
|
|
824
|
+
export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
|
|
825
|
+
effort: Effort,
|
|
826
|
+
model?: ApiModel<TApi>,
|
|
827
|
+
): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
|
|
828
|
+
if (effort === Effort.Minimal) {
|
|
829
|
+
const routing = model?.thinking?.effortRouting;
|
|
830
|
+
if (routing?.[Effort.Minimal] && routing[Effort.Minimal] === routing[Effort.Low]) {
|
|
831
|
+
return "LOW";
|
|
832
|
+
}
|
|
833
|
+
return "MINIMAL";
|
|
834
|
+
}
|
|
801
835
|
switch (effort) {
|
|
802
|
-
case Effort.Minimal:
|
|
803
|
-
return "MINIMAL";
|
|
804
836
|
case Effort.Low:
|
|
805
837
|
return "LOW";
|
|
806
838
|
case Effort.Medium:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { bareModelId, parseAnthropicModel, parseGlmModel, semverGte } from "./identity/classify";
|
|
2
|
+
import type { ModelTokenizer } from "./types";
|
|
3
|
+
|
|
4
|
+
const DEEPSEEK_V3_ALIASES: Record<string, true> = {
|
|
5
|
+
"deepseek-chat": true,
|
|
6
|
+
"deepseek-reasoner": true,
|
|
7
|
+
};
|
|
8
|
+
const KIMI_K2_ALIASES: Record<string, true> = {
|
|
9
|
+
"kimi-for-coding": true,
|
|
10
|
+
"kimi-for-coding-highspeed": true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Resolution is pure and catalog ids recur across providers and aliases.
|
|
14
|
+
const MAX_TOKENIZER_CACHE_ENTRIES = 2_048;
|
|
15
|
+
const modelTokenizerCache = new Map<string, ModelTokenizer | null>();
|
|
16
|
+
|
|
17
|
+
function claudeTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
18
|
+
const parsed = parseAnthropicModel(modelId);
|
|
19
|
+
if (parsed) {
|
|
20
|
+
if (parsed.kind === "opus") {
|
|
21
|
+
if (semverGte(parsed.version, "5")) return "claude-v5";
|
|
22
|
+
if (semverGte(parsed.version, "4.7")) return "claude-v47";
|
|
23
|
+
return "claude-v3";
|
|
24
|
+
}
|
|
25
|
+
return semverGte(parsed.version, "5") ? "claude-v5-sonnet" : "claude-v3";
|
|
26
|
+
}
|
|
27
|
+
return /(^|[-/.:])claude([-.:]|$)/i.test(modelId) ? "claude-v3" : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function qwenTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
31
|
+
const version = /qwen[-_ ]?(\d+)\.(\d+)(?![\dbB])/i.exec(modelId);
|
|
32
|
+
if (!version) return undefined;
|
|
33
|
+
const major = Number.parseInt(version[1], 10);
|
|
34
|
+
const minor = Number.parseInt(version[2], 10);
|
|
35
|
+
return major > 3 || (major === 3 && minor >= 5) ? "qwen3" : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deepSeekTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
39
|
+
const lower = modelId.toLowerCase();
|
|
40
|
+
if (lower.includes("distill")) return undefined;
|
|
41
|
+
if (DEEPSEEK_V3_ALIASES[lower]) return "deepseek-v3";
|
|
42
|
+
return /(?:^|[-_.:])(?:v?[34]|r1)(?:[-_.:]|$)/.test(lower) && lower.includes("deepseek") ? "deepseek-v3" : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function kimiTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
46
|
+
const lower = modelId.toLowerCase();
|
|
47
|
+
if (KIMI_K2_ALIASES[lower]) return "kimi-k2";
|
|
48
|
+
return /(?:^|[-_.:])kimi[-_.:]?k?[23](?:[-_.:]|$)/.test(lower) ? "kimi-k2" : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function glmTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
52
|
+
const glm = parseGlmModel(modelId);
|
|
53
|
+
return glm && semverGte(glm.version, "5") ? "glm5" : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the exact locally embedded tokenizer for a canonical model id.
|
|
58
|
+
*
|
|
59
|
+
* This is catalog policy, not a runtime caller heuristic: [`buildModel`](./build.ts)
|
|
60
|
+
* materializes the result as `Model.tokenizer`; consumers read that property.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveModelTokenizer(modelId: string): ModelTokenizer | undefined {
|
|
63
|
+
const cached = modelTokenizerCache.get(modelId);
|
|
64
|
+
if (cached !== undefined) return cached ?? undefined;
|
|
65
|
+
const bare = bareModelId(modelId);
|
|
66
|
+
const tokenizer =
|
|
67
|
+
claudeTokenizer(bare) ??
|
|
68
|
+
qwenTokenizer(bare) ??
|
|
69
|
+
deepSeekTokenizer(bare) ??
|
|
70
|
+
kimiTokenizer(bare) ??
|
|
71
|
+
glmTokenizer(bare);
|
|
72
|
+
if (modelTokenizerCache.size === MAX_TOKENIZER_CACHE_ENTRIES) modelTokenizerCache.clear();
|
|
73
|
+
modelTokenizerCache.set(modelId, tokenizer ?? null);
|
|
74
|
+
return tokenizer;
|
|
75
|
+
}
|