@oh-my-pi/pi-catalog 18.2.6 → 18.2.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 +24 -3
- package/README.md +18 -18
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/build.d.ts +7 -1
- package/dist/types/compat/auth-ids.d.ts +1 -1
- package/dist/types/compat/axes.d.ts +1 -1
- package/dist/types/compat/cascade.d.ts +3 -2
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/resolve.d.ts +2 -0
- package/dist/types/compat/taxonomy.d.ts +2 -2
- package/dist/types/compat/types.d.ts +18 -7
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/discovery/typesafe.d.ts +26 -0
- package/dist/types/model-manager.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +6 -4
- package/dist/types/provider-models/special.d.ts +10 -0
- package/dist/types/types.d.ts +23 -0
- package/package.json +38 -38
- package/src/build.ts +27 -5
- package/src/compat/auth-ids.ts +2 -0
- package/src/compat/axes.ts +9 -1
- package/src/compat/cascade.ts +45 -23
- package/src/compat/provider-ids.ts +3 -0
- package/src/compat/resolve.ts +24 -14
- package/src/compat/rules/README.md +38 -35
- package/src/compat/rules/auth/local.kdl +4 -0
- package/src/compat/rules/auth/typesafe.kdl +3 -6
- package/src/compat/rules/auth/web.kdl +4 -0
- package/src/compat/rules/classes/qwen.kdl +6 -5
- package/src/compat/rules/providers/anthropic.kdl +1 -0
- package/src/compat/rules/providers/deepinfra.kdl +28 -0
- package/src/compat/rules/providers/google-antigravity.kdl +17 -0
- package/src/compat/rules/providers/google.kdl +9 -0
- package/src/compat/rules/providers/llama.cpp.kdl +23 -0
- package/src/compat/rules/providers/local.kdl +110 -0
- package/src/compat/rules/providers/openai-codex.kdl +18 -0
- package/src/compat/rules/providers/openai.kdl +61 -0
- package/src/compat/rules/providers/openrouter.kdl +132 -0
- package/src/compat/rules/providers/typesafe.kdl +21 -0
- package/src/compat/rules/providers/web.kdl +153 -0
- package/src/compat/rules/providers/xai-oauth.kdl +26 -0
- package/src/compat/rules/providers/xai.kdl +22 -0
- package/src/compat/rules/runtime/behavior.kdl +2 -1
- package/src/compat/rules/taxonomy/qwen.kdl +2 -0
- package/src/compat/rules.json +12526 -1
- package/src/compat/taxonomy.ts +92 -31
- package/src/compat/types.ts +22 -7
- package/src/discovery/devin.ts +3 -1
- package/src/discovery/index.ts +1 -0
- package/src/discovery/openai-compatible.ts +4 -1
- package/src/discovery/typesafe.ts +118 -0
- package/src/model-manager.ts +44 -15
- package/src/models.json +1 -1
- package/src/provider-models/descriptors.ts +6 -0
- package/src/provider-models/openai-compat.ts +357 -78
- package/src/provider-models/special.ts +53 -0
- package/src/types.ts +51 -0
package/src/build.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* compat per request.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { resolveModelPolicy } from "./compat/resolve";
|
|
10
|
+
import { resolveDiscoveryApi, resolveModelPolicy } from "./compat/resolve";
|
|
11
11
|
import type { ModelIdentity } from "./compat/types";
|
|
12
12
|
import { resolveModelTokenizer } from "./model-tokenizer";
|
|
13
13
|
import { materializeTimeBasedCost } from "./pricing";
|
|
14
|
-
import type
|
|
14
|
+
import { type Api, MODEL_KINDS, type Model, type ModelSpec } from "./types";
|
|
15
15
|
import { cleanModelName } from "./utils";
|
|
16
16
|
|
|
17
17
|
function numberField(source: object, key: string): number | undefined {
|
|
@@ -74,6 +74,18 @@ function isInputModalities(value: unknown): value is ("text" | "image")[] {
|
|
|
74
74
|
* only when the spec left it unset.
|
|
75
75
|
*/
|
|
76
76
|
function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog: Record<string, unknown>): void {
|
|
77
|
+
const kind = MODEL_KINDS.find(value => value === catalog.kind);
|
|
78
|
+
if (kind !== undefined) model.kind = kind;
|
|
79
|
+
const webSearch = catalog.webSearch;
|
|
80
|
+
if (
|
|
81
|
+
webSearch === "gemini" ||
|
|
82
|
+
webSearch === "anthropic" ||
|
|
83
|
+
webSearch === "codex" ||
|
|
84
|
+
webSearch === "xai" ||
|
|
85
|
+
webSearch === "openrouter"
|
|
86
|
+
) {
|
|
87
|
+
model.webSearch = webSearch;
|
|
88
|
+
}
|
|
77
89
|
const serviceTierCost = objectPayload(catalog.serviceTierCost);
|
|
78
90
|
if (serviceTierCost !== undefined) {
|
|
79
91
|
const flex = numberField(serviceTierCost, "flex");
|
|
@@ -273,6 +285,16 @@ function supportsOpenAIGAComputerUse(
|
|
|
273
285
|
return wireIdentity.class === "openai" && revisionAtLeast(wireIdentity, 5, 4);
|
|
274
286
|
}
|
|
275
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Build a discovered model using the backend's catalog-selected request API.
|
|
290
|
+
* The credential-bearing provider id remains unchanged while `providerType`
|
|
291
|
+
* persists the backend policy identity across cache and config round trips.
|
|
292
|
+
*/
|
|
293
|
+
export function buildDiscoveredModel(spec: ModelSpec<Api>, providerType: string): Model<Api> {
|
|
294
|
+
const api = resolveDiscoveryApi(spec, providerType);
|
|
295
|
+
return buildModel({ ...spec, api, providerType });
|
|
296
|
+
}
|
|
297
|
+
|
|
276
298
|
/**
|
|
277
299
|
* Build one model from an authored spec. Bundled models.json rows are fully
|
|
278
300
|
* materialized by the generator and consumed directly (see `models.ts`), so
|
|
@@ -283,9 +305,9 @@ export function buildModel<TApi extends Api>(spec: ModelSpec<TApi>): Model<TApi>
|
|
|
283
305
|
const supportsComputerUseConfig = explicitComputerUseConfig(spec);
|
|
284
306
|
const model: Model<TApi> = {
|
|
285
307
|
...spec,
|
|
286
|
-
//
|
|
287
|
-
// discovery default (see `resolveThinkingPolicy`);
|
|
288
|
-
// correction
|
|
308
|
+
// A reviewed `thinking-upgrade-neutral` policy can repair a stale
|
|
309
|
+
// `reasoning: false` discovery default (see `resolveThinkingPolicy`);
|
|
310
|
+
// materialize the correction for transports and the picker.
|
|
289
311
|
reasoning: spec.reasoning || policy.thinking !== undefined,
|
|
290
312
|
name: cleanModelName(spec.name),
|
|
291
313
|
identity: policy.identity,
|
package/src/compat/auth-ids.ts
CHANGED
|
@@ -41,6 +41,7 @@ export type AuthProviderId =
|
|
|
41
41
|
| "litellm"
|
|
42
42
|
| "llama.cpp"
|
|
43
43
|
| "lm-studio"
|
|
44
|
+
| "local"
|
|
44
45
|
| "meta"
|
|
45
46
|
| "minimax"
|
|
46
47
|
| "minimax-code"
|
|
@@ -76,6 +77,7 @@ export type AuthProviderId =
|
|
|
76
77
|
| "vercel-ai-gateway"
|
|
77
78
|
| "vllm"
|
|
78
79
|
| "wafer-serverless"
|
|
80
|
+
| "web"
|
|
79
81
|
| "xai"
|
|
80
82
|
| "xai-oauth"
|
|
81
83
|
| "xiaomi"
|
package/src/compat/axes.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* one of the axis's declared records.
|
|
11
11
|
*/
|
|
12
12
|
import type { Effort } from "../effort";
|
|
13
|
-
import type
|
|
13
|
+
import { MODEL_KINDS, type ThinkingControlMode } from "../types";
|
|
14
14
|
|
|
15
15
|
/** Value shape a directive accepts (see `rules/README.md`). */
|
|
16
16
|
export type AxisShape = "scalar" | "array" | "object";
|
|
@@ -290,9 +290,17 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
|
|
|
290
290
|
"cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
|
|
291
291
|
"cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
|
|
292
292
|
"delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
|
|
293
|
+
"discovery-api": { key: "discoveryApi", set: "catalog", shape: "scalar" },
|
|
293
294
|
"edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
|
|
294
295
|
"edit-revision": { key: "editRevision", set: "catalog", shape: "scalar" },
|
|
295
296
|
"input-modalities": { key: "inputModalities", set: "catalog", shape: "array", values: ["text", "image"] },
|
|
297
|
+
kind: { key: "kind", set: "catalog", shape: "scalar", values: MODEL_KINDS },
|
|
298
|
+
"web-search": {
|
|
299
|
+
key: "webSearch",
|
|
300
|
+
set: "catalog",
|
|
301
|
+
shape: "scalar",
|
|
302
|
+
values: ["gemini", "anthropic", "codex", "xai", "openrouter"],
|
|
303
|
+
},
|
|
296
304
|
"limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
|
|
297
305
|
"long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
|
|
298
306
|
"long-usage-limit-fallback": { key: "longUsageLimitFallback", set: "catalog", shape: "scalar" },
|
package/src/compat/cascade.ts
CHANGED
|
@@ -239,6 +239,30 @@ function rankCompare(a: readonly [number, number, number], b: readonly [number,
|
|
|
239
239
|
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function contestAxis(
|
|
243
|
+
winners: WinnerTable,
|
|
244
|
+
axis: string,
|
|
245
|
+
rank: readonly [number, number, number],
|
|
246
|
+
rule: IndexedRule,
|
|
247
|
+
target: ResolveTarget,
|
|
248
|
+
): void {
|
|
249
|
+
const held = winners[axis];
|
|
250
|
+
if (held) {
|
|
251
|
+
const order = rankCompare(held.rank, rank);
|
|
252
|
+
if (order === 0) {
|
|
253
|
+
throw new AmbiguousOverlapError(
|
|
254
|
+
target.provider,
|
|
255
|
+
target.model,
|
|
256
|
+
axis,
|
|
257
|
+
held.rule.compiled.source,
|
|
258
|
+
rule.compiled.source,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (order > 0) return;
|
|
262
|
+
}
|
|
263
|
+
winners[axis] = { rank, rule };
|
|
264
|
+
}
|
|
265
|
+
|
|
242
266
|
function contest(
|
|
243
267
|
winners: WinnerTable,
|
|
244
268
|
axes: Record<string, unknown> | undefined,
|
|
@@ -247,23 +271,7 @@ function contest(
|
|
|
247
271
|
target: ResolveTarget,
|
|
248
272
|
): void {
|
|
249
273
|
if (!axes) return;
|
|
250
|
-
for (const axis in axes)
|
|
251
|
-
const held = winners[axis];
|
|
252
|
-
if (held) {
|
|
253
|
-
const order = rankCompare(held.rank, rank);
|
|
254
|
-
if (order === 0) {
|
|
255
|
-
throw new AmbiguousOverlapError(
|
|
256
|
-
target.provider,
|
|
257
|
-
target.model,
|
|
258
|
-
axis,
|
|
259
|
-
held.rule.compiled.source,
|
|
260
|
-
rule.compiled.source,
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (order > 0) continue;
|
|
264
|
-
}
|
|
265
|
-
winners[axis] = { rank, rule };
|
|
266
|
-
}
|
|
274
|
+
for (const axis in axes) contestAxis(winners, axis, rank, rule, target);
|
|
267
275
|
}
|
|
268
276
|
|
|
269
277
|
function collect(winners: WinnerTable, pick: (rule: CompiledRule) => Record<string, unknown> | undefined) {
|
|
@@ -314,8 +322,9 @@ function cloneAxes(axes: ResolvedAxes): ResolvedAxes {
|
|
|
314
322
|
|
|
315
323
|
/**
|
|
316
324
|
* Resolve wire, thinking, and catalog assignments for one structured target.
|
|
317
|
-
* Exact model effort corrections
|
|
318
|
-
*
|
|
325
|
+
* Exact model effort corrections and identity-scoped neutral-upgrade policies
|
|
326
|
+
* can enable reasoning; absent family/revision facts never satisfy selectors
|
|
327
|
+
* that require them. Returned axes are caller-owned.
|
|
319
328
|
*
|
|
320
329
|
* @throws AmbiguousOverlapError when equal-rank rules contest one axis.
|
|
321
330
|
*/
|
|
@@ -368,11 +377,24 @@ function resolveOverIndex(index: RuleIndex, target: ResolveTarget): ResolvedAxes
|
|
|
368
377
|
const ranked = rankRelevantRules(index, prepareTarget(target));
|
|
369
378
|
let reasoning = target.reasoning === true;
|
|
370
379
|
if (!reasoning) {
|
|
380
|
+
const upgrade: WinnerTable = {};
|
|
381
|
+
let hasEfforts = false;
|
|
371
382
|
for (const { rule, rank } of ranked) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
const thinking = rule.compiled.thinking;
|
|
384
|
+
if (thinking === undefined) continue;
|
|
385
|
+
if ("upgradeNeutral" in thinking) contestAxis(upgrade, "upgradeNeutral", rank, rule, target);
|
|
386
|
+
if ("efforts" in thinking) hasEfforts = true;
|
|
387
|
+
if (rule.hasExactEffortsRule && rank[0] === 2) reasoning = true;
|
|
388
|
+
}
|
|
389
|
+
const upgradeRule = upgrade.upgradeNeutral?.rule.compiled;
|
|
390
|
+
const identityScoped =
|
|
391
|
+
upgradeRule !== undefined &&
|
|
392
|
+
((upgradeRule.class !== undefined && upgradeRule.class !== "unknown") ||
|
|
393
|
+
upgradeRule.family !== undefined ||
|
|
394
|
+
upgradeRule.revision !== undefined ||
|
|
395
|
+
upgradeRule.models !== undefined);
|
|
396
|
+
if (identityScoped && upgradeRule?.thinking?.upgradeNeutral === true && hasEfforts) {
|
|
397
|
+
reasoning = true;
|
|
376
398
|
}
|
|
377
399
|
}
|
|
378
400
|
const wire: WinnerTable = {};
|
|
@@ -38,6 +38,7 @@ export type KnownProvider =
|
|
|
38
38
|
| "kimi-code"
|
|
39
39
|
| "litellm"
|
|
40
40
|
| "lm-studio"
|
|
41
|
+
| "local"
|
|
41
42
|
| "meta"
|
|
42
43
|
| "minimax"
|
|
43
44
|
| "minimax-code"
|
|
@@ -62,11 +63,13 @@ export type KnownProvider =
|
|
|
62
63
|
| "siliconflow-cn"
|
|
63
64
|
| "synthetic"
|
|
64
65
|
| "together"
|
|
66
|
+
| "typesafe"
|
|
65
67
|
| "umans"
|
|
66
68
|
| "venice"
|
|
67
69
|
| "vercel-ai-gateway"
|
|
68
70
|
| "vllm"
|
|
69
71
|
| "wafer-serverless"
|
|
72
|
+
| "web"
|
|
70
73
|
| "xai"
|
|
71
74
|
| "xai-oauth"
|
|
72
75
|
| "xiaomi"
|
package/src/compat/resolve.ts
CHANGED
|
@@ -311,6 +311,7 @@ function detectOpenAI(
|
|
|
311
311
|
reasoningCapable: boolean,
|
|
312
312
|
): OpenAIDetection {
|
|
313
313
|
const provider = spec.provider;
|
|
314
|
+
const backendProvider = spec.providerType ?? provider;
|
|
314
315
|
const baseUrl = spec.baseUrl;
|
|
315
316
|
const hostModel = { provider, baseUrl };
|
|
316
317
|
const isZai = modelMatchesHost(hostModel, "zai");
|
|
@@ -321,8 +322,8 @@ function detectOpenAI(
|
|
|
321
322
|
const isDeepseekFamily = modelMatchesHost(hostModel, "deepseekFamily") || facts.is("deepseek");
|
|
322
323
|
const isDeepseekReasoning = isDeepseekFamily && reasoningCapable;
|
|
323
324
|
const isLocalOpenAICompatBackend =
|
|
324
|
-
PROXY_OPENAI_COMPAT_PROVIDERS[
|
|
325
|
-
(LOCAL_OPENAI_COMPAT_PROVIDERS[
|
|
325
|
+
PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true &&
|
|
326
|
+
(LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true || hasLocalLoopbackBaseUrl(baseUrl));
|
|
326
327
|
return {
|
|
327
328
|
facts,
|
|
328
329
|
isClinePass: provider === "cline-pass",
|
|
@@ -521,12 +522,8 @@ function detectOpenAICompat(
|
|
|
521
522
|
replayReasoningContent: d.isLocalOpenAICompatBackend,
|
|
522
523
|
qwenPreserveThinking:
|
|
523
524
|
(thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && d.isLocalOpenAICompatBackend,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
d.isLocalOpenAICompatBackend &&
|
|
527
|
-
provider !== "ollama" &&
|
|
528
|
-
isQwen &&
|
|
529
|
-
facts.revGte("3.8"),
|
|
525
|
+
// Template effort support is a reviewed backend × model contract in KDL.
|
|
526
|
+
qwenTemplateReasoningEffort: false,
|
|
530
527
|
requiresAssistantContentForToolCalls: facts.is("kimi") || d.isDirectDeepseekReasoning,
|
|
531
528
|
cacheControlFormat:
|
|
532
529
|
(d.isClinePass && (isQwen || isAnthropicModel)) || (d.isOpenRouter && isAnthropicModel)
|
|
@@ -692,6 +689,7 @@ function resolveOpenAIResponsesPolicy(
|
|
|
692
689
|
): ResolvedOpenAIResponsesCompat {
|
|
693
690
|
const baseUrl = spec.baseUrl ?? "";
|
|
694
691
|
const provider = spec.provider;
|
|
692
|
+
const backendProvider = spec.providerType ?? provider;
|
|
695
693
|
const hostModel = { provider, baseUrl };
|
|
696
694
|
const isAzure = modelMatchesHost(hostModel, "azureOpenAI");
|
|
697
695
|
const isOpenRouter = modelMatchesHost(hostModel, "openrouter");
|
|
@@ -703,7 +701,8 @@ function resolveOpenAIResponsesPolicy(
|
|
|
703
701
|
const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = isOpenRouter ? "openrouter" : "openai";
|
|
704
702
|
const reasoningCapable = compatReasoning(spec, axes);
|
|
705
703
|
const isLocalServingBackend =
|
|
706
|
-
(PROXY_OPENAI_COMPAT_PROVIDERS[
|
|
704
|
+
(PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true &&
|
|
705
|
+
LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true) ||
|
|
707
706
|
hasLocalLoopbackBaseUrl(baseUrl);
|
|
708
707
|
const isAnthropicModel = facts.is("anthropic");
|
|
709
708
|
const isDeepseekFamily = facts.is("deepseek");
|
|
@@ -1063,8 +1062,8 @@ function readRuleThinking(axes: ResolvedAxes): RuleThinking {
|
|
|
1063
1062
|
|
|
1064
1063
|
/**
|
|
1065
1064
|
* Compat-time reasoning capability. `axes.reasoning` also promotes targets on
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1065
|
+
* reviewed effort corrections (the cascade's thinking-axis gate), but compat
|
|
1066
|
+
* may only be repaired where the matching contract opted in with
|
|
1068
1067
|
* `thinking-upgrade-neutral`; everywhere else a spec that reports no reasoning
|
|
1069
1068
|
* stays the authoritative capability surface.
|
|
1070
1069
|
*/
|
|
@@ -1104,7 +1103,7 @@ function resolveThinkingPolicy<TApi extends Api>(
|
|
|
1104
1103
|
// reasoning (e.g. Synthetic's `none`-only off-switch): reviewed KDL must
|
|
1105
1104
|
// not re-expand it into an unadvertised ladder. Absent metadata is
|
|
1106
1105
|
// repaired only where KDL opts in with `thinking-upgrade-neutral`
|
|
1107
|
-
// alongside
|
|
1106
|
+
// alongside a reviewed `thinking-efforts` ladder (the cascade upgrade for
|
|
1108
1107
|
// stale source capability data); otherwise the neutral default holds.
|
|
1109
1108
|
if (!spec.reasoning && (explicitThinking !== undefined || rule.upgradeNeutral !== true)) return undefined;
|
|
1110
1109
|
if (
|
|
@@ -1210,9 +1209,13 @@ function fillExplicitThinking<TApi extends Api>(
|
|
|
1210
1209
|
// Entry
|
|
1211
1210
|
// ---------------------------------------------------------------------------
|
|
1212
1211
|
|
|
1213
|
-
function buildResolveTarget<TApi extends Api>(
|
|
1212
|
+
function buildResolveTarget<TApi extends Api>(
|
|
1213
|
+
spec: ModelSpec<TApi>,
|
|
1214
|
+
identity: ModelIdentity,
|
|
1215
|
+
providerType = spec.providerType ?? spec.provider,
|
|
1216
|
+
): ResolveTarget {
|
|
1214
1217
|
const target: ResolveTarget = {
|
|
1215
|
-
provider:
|
|
1218
|
+
provider: providerType,
|
|
1216
1219
|
api: spec.api,
|
|
1217
1220
|
class: identity.class,
|
|
1218
1221
|
model: spec.id,
|
|
@@ -1227,6 +1230,13 @@ function specUsesApi<TApi extends Api>(spec: ModelSpec<Api>, api: TApi): spec is
|
|
|
1227
1230
|
return spec.api === api;
|
|
1228
1231
|
}
|
|
1229
1232
|
|
|
1233
|
+
/** Resolve the request adapter assigned to a discovery backend before materialization. */
|
|
1234
|
+
export function resolveDiscoveryApi(spec: ModelSpec<Api>, providerType: string): Api {
|
|
1235
|
+
const identity = resolveIdentity(spec);
|
|
1236
|
+
const discoveryApi = resolveCascade(buildResolveTarget(spec, identity, providerType)).catalog.discoveryApi;
|
|
1237
|
+
return typeof discoveryApi === "string" ? discoveryApi : spec.api;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1230
1240
|
/**
|
|
1231
1241
|
* Resolves the full policy surface for one model spec: structured identity,
|
|
1232
1242
|
* complete compat record, thinking metadata, and catalog-data corrections.
|
|
@@ -50,14 +50,14 @@ class "anthropic" {
|
|
|
50
50
|
|
|
51
51
|
Classification trims and lowercases the full model identifier. The **bare name** is the segment after its final `/`. Matcher tokens are also lowercased while parsing.
|
|
52
52
|
|
|
53
|
-
| Node
|
|
54
|
-
|
|
|
55
|
-
| `exact "token"`
|
|
56
|
-
| `bounded "token"`
|
|
57
|
-
| `namespace "token"`
|
|
58
|
-
| `namespace "token" bounded=#true` |
|
|
59
|
-
| `prefix "token"`
|
|
60
|
-
| `glob "pattern"`
|
|
53
|
+
| Node | Rank | Match |
|
|
54
|
+
| --------------------------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `exact "token"` | 4 | The whole bare name equals `token`. |
|
|
56
|
+
| `bounded "token"` | 3 | The bare name equals `token`, or starts with it followed by `-`, `_`, `.`, `:`, or an ASCII digit. |
|
|
57
|
+
| `namespace "token"` | 2 | A non-empty `/`-separated segment of the full identifier equals `token`. |
|
|
58
|
+
| `namespace "token" bounded=#true` | 2 | Split the full identifier on `/`, `.`, and `:`; a segment must satisfy the bounded rule above. This is the only matcher property. |
|
|
59
|
+
| `prefix "token"` | 1 | The bare name starts with `token`. |
|
|
60
|
+
| `glob "pattern"` | 0 | An anchored `*` wildcard match over the bare name. `*` spans any substring; all non-wildcard text remains anchored in order. |
|
|
61
61
|
|
|
62
62
|
A class match is ranked by `(matcher-kind rank, token byte length)`. The greatest tuple wins. Equal tuples from different classes are an ambiguity error; source order is not a tiebreak. If nothing matches, classification returns class `unknown` with no family or revision.
|
|
63
63
|
|
|
@@ -89,22 +89,22 @@ revision skip-bare "o1" "o3" "o4"
|
|
|
89
89
|
|
|
90
90
|
### Reviewed identity overrides
|
|
91
91
|
|
|
92
|
-
`override` has properties only and no child block. Required string properties are `id` (stable, globally unique review ID), `model` (exact bare
|
|
92
|
+
`override` has properties only and no child block. Required string properties are `id` (stable, globally unique review ID), `rationale`, and `provenance`, plus exactly one selector: `model` (exact bare identifier) or `glob` (anchored `*` wildcard over the bare identifier). Both selectors compare case-insensitively; namespace prefixes before the final `/` are ignored.
|
|
93
93
|
|
|
94
94
|
Optional properties are:
|
|
95
95
|
|
|
96
|
-
| Property
|
|
97
|
-
|
|
|
98
|
-
| `provider`
|
|
99
|
-
| `logical`
|
|
100
|
-
| `class`
|
|
101
|
-
| `family`
|
|
102
|
-
| `revision`
|
|
103
|
-
| `effort`
|
|
104
|
-
| `thinking-variant` | Boolean marker for a separately exposed thinking sibling.
|
|
105
|
-
| `expires-at-ms`
|
|
96
|
+
| Property | Shape and meaning |
|
|
97
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `provider` | Exact provider key, compared case-insensitively. A matching provider-specific override wins over a provider-agnostic one. |
|
|
99
|
+
| `logical` | Corrected logical model identifier. |
|
|
100
|
+
| `class` | Corrected class ID; a non-empty string. |
|
|
101
|
+
| `family` | Corrected product-family ID; a non-empty string. |
|
|
102
|
+
| `revision` | One to three unsigned 8-bit components separated by `.` or `-`. |
|
|
103
|
+
| `effort` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
|
|
104
|
+
| `thinking-variant` | Boolean marker for a separately exposed thinking sibling. |
|
|
105
|
+
| `expires-at-ms` | Non-negative Unix time in milliseconds. The override is inactive when the observation time is at or after this value. |
|
|
106
106
|
|
|
107
|
-
The
|
|
107
|
+
The tuple `(provider, selector-kind, selector)` must also be unique, including provider-agnostic selectors. Active exact overrides take precedence over every glob. Within each selector kind, provider-scoped overrides precede provider-agnostic ones; matching globs rank by non-wildcard byte count. Equal-ranked globs are an ambiguity error, never resolved by declaration order. When no observation time is supplied, an expiring override remains active.
|
|
108
108
|
|
|
109
109
|
### Suffix collapse
|
|
110
110
|
|
|
@@ -124,6 +124,7 @@ collapse {
|
|
|
124
124
|
provider-alias "devin" "opus" "claude-opus-5"
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
|
+
|
|
127
128
|
`variant-family` declares one reviewed provider-scoped collapsed family: positional provider and logical id, `name=` display name, and a body of `members "a" "b" …` (wire ids in priority order), `route "<tier>" "<wire-id>"` per effort tier (`off` included), and optional `mode`, `efforts`, `default-level`, `default-member`, `retired-members`, `effort-budget "<tier>" <n>`, `requires-effort`, `suppress-when-off`, `no-thinking`, `preserve-absent-effort-routes`, and `extra-aliases`. A `{rev}` placeholder in the logical id makes the node a **template**: it is instantiated once per revision found in live ids (`gemini-{rev}-flash` matches `gemini-3.8-flash-low` → family `gemini-3.8-flash`), every wire id in the body and the `name=` carry the same placeholder, and an optional `revision=` constraint (`">=3.6"`) bounds the generations it applies to. A concrete family with the same instantiated id wins over the template. `provider-alias` maps one provider-scoped selector spelling onto a logical id without making it a family member.
|
|
128
129
|
|
|
129
130
|
`thinking-suffix` accepts one non-empty suffix and no properties. `pair-token` declares bounded (possibly infix) tokens naming the thinking sibling of a live bare twin (`sonar-reasoning-pro` beside `sonar-pro`); it drives thinking-pair derivation only — never identity suffix collapse — and negated `no-`/`non-` forms never match. `effort-suffix` additionally requires `tier` with one of the effort values above, and may have `except-bare-prefix`. `routing-variant-suffix` takes one non-empty suffix followed by one or more provider IDs: a wire identifier carrying the suffix on one of those providers is a **routing variant** of its plain identifier — discovery derives base-model metadata from the plain bundled SKU while keeping the suffixed wire identifier for requests; routing variants never participate in effort collapse. `effort-lane-suffix` takes one non-empty lane suffix followed by one or more provider IDs, plus an optional `bare-prefix` gate: on a declared provider, an identifier ending in the lane suffix collapses the effort suffix wedged before the lane token while keeping the lane on the logical id. `effort-family` takes a provider, the canonical logical id, and zero or more exact aliases that fold onto it.
|
|
@@ -178,15 +179,15 @@ provider "openrouter" {
|
|
|
178
179
|
|
|
179
180
|
### Selectors and nesting
|
|
180
181
|
|
|
181
|
-
| Selector
|
|
182
|
-
|
|
|
183
|
-
| `class`
|
|
184
|
-
| `provider` | `provider "id" { ... }`
|
|
185
|
-
| `on`
|
|
186
|
-
| `on-api`
|
|
187
|
-
| `family`
|
|
188
|
-
| `revision` | `revision ">=2.5 <4" { ... }`
|
|
189
|
-
| `models`
|
|
182
|
+
| Selector | Form | Matching semantics |
|
|
183
|
+
| ---------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
184
|
+
| `class` | `class "id" { ... }` | Exact class ID. At document root it may contain `on`, `on-api`, `family`, `revision`, and `models`. Under `provider` it may contain `family`, `revision`, and `models`. |
|
|
185
|
+
| `provider` | `provider "id" { ... }` | Exact provider ID. It is root-only and may contain `class` and `models`. |
|
|
186
|
+
| `on` | `on "provider-a" "provider-b" { ... }` | One or more provider IDs, combined as OR. It is allowed only under a root `class`, and may contain `family`, `revision`, and `models`. |
|
|
187
|
+
| `on-api` | `on-api "adapter-a" "adapter-b" { ... }` | One or more request adapter IDs, combined as OR. It is allowed only under a root `class`, and may contain `family`, `revision`, and `models`. |
|
|
188
|
+
| `family` | `family "id" { ... }` | Exact classified family ID. It may contain `revision` and `models`. A target with no family does not match. |
|
|
189
|
+
| `revision` | `revision ">=2.5 <4" { ... }` | A non-empty, whitespace-separated conjunction of comparisons. It may contain `models`. A target with no revision does not match. |
|
|
190
|
+
| `models` | `models "id" "vendor/*" { ... }` | One or more alternatives, combined as OR. It cannot contain another selector. `token="name"` matches an ASCII-case-insensitive token bounded by non-alphanumerics. |
|
|
190
191
|
|
|
191
192
|
Class, provider/`on`, `on-api`, and family selector values are compared exactly and case-sensitively to the structured resolve target. Revision operators are `>=`, `>`, `<=`, `<`, and `=`; operands have one to three dot-separated unsigned 8-bit components, omitted components zero.
|
|
192
193
|
|
|
@@ -194,6 +195,8 @@ A `models` string without `*` is an exact, case-sensitive match against the prov
|
|
|
194
195
|
|
|
195
196
|
`priority=N` is an optional signed integer property on the block that owns axis assignments. Its default is zero. Use it only to resolve an intentional equal-specificity overlap; do not use it to encode declaration order.
|
|
196
197
|
|
|
198
|
+
`buildDiscoveredModel(spec, providerType)` resolves the catalog `discovery-api` axis before materializing compatibility. It preserves the credential-bearing provider ID and records `providerType` as the backend used for provider selectors on subsequent rebuilds. Ordinary `buildModel` preserves its input API. This lets custom-named llama.cpp deployments reuse the same rules without model-specific discovery code.
|
|
199
|
+
|
|
197
200
|
### Axis vocabulary and value shapes
|
|
198
201
|
|
|
199
202
|
The directive vocabulary is closed and lives in **`src/compat/axes.ts`** — one table mapping each kebab-case directive to its resolved camelCase field, namespace (`wire` / `thinking` / `catalog`), value shape, applicable compat records, and (for enums) accepted values. The compiler rejects unknown directives and out-of-vocabulary values against that table; consult it rather than a duplicated table here.
|
|
@@ -262,7 +265,7 @@ The highest-ranked matching assignment wins for that axis. Two distinct rules th
|
|
|
262
265
|
|
|
263
266
|
### Capability gating
|
|
264
267
|
|
|
265
|
-
Wire axes are considered for every matching target. Thinking axes
|
|
268
|
+
Wire axes are considered for every matching target. Thinking axes require `reasoning`, except that an exact model selector declaring `thinking-efforts` opens the gate for reviewed corrections. A winning `thinking-upgrade-neutral #true` scoped to a recognized class, family, revision, or model selector also opens it when a matching effort ladder exists; provider-wide opt-in alone does not. Materializing a neutral spec as reasoning-capable requires that opt-in and no explicit thinking vocabulary. Family and revision selectors never match targets missing that rank. An unmatched target resolves to empty maps; the cascade does not infer negative capabilities from absence.
|
|
266
269
|
|
|
267
270
|
## Runtime behavior grammar
|
|
268
271
|
|
|
@@ -399,17 +402,17 @@ Only `discovery` enrolls a provider in `generate-models.ts`; providers without i
|
|
|
399
402
|
|
|
400
403
|
### Seed rows
|
|
401
404
|
|
|
402
|
-
A `seed`
|
|
405
|
+
A `seed` _defines_ bundled rows for providers whose catalog cannot be discovered at generation time — credential-scoped rosters, unauthenticated regens, or models ahead of upstream catalogs. Every other stratum patches rows; this one authors them. Runtime model managers hand the rows to `staticModels` through `seedModels(provider)`; the generator bundles them per the seed's `bundle` policy. Values are literal — a seed never derives from another provider's row, and pricing is never borrowed.
|
|
403
406
|
|
|
404
407
|
`seed` properties: `api` and `base-url` are per-row defaults (a `model` may override either with the same property names); `bundle` defaults to `always`; `precedence="seed"` is optional. `model` takes the wire id positionally, requires `name=`, and its body MUST declare `reasoning`, `input` (`"text"` and/or `"image"`), `cost` (all four per-million rates), and `limits` (`context=` / `max-tokens=`, an omitted limit is `null`); `supports-tools #true` is optional. Any other directive is an axis from the cascade vocabulary: thinking axes become the row's explicit `thinking` (then `thinking-mode` and `thinking-efforts` are both required), wire axes become its explicit `compat` and must apply to the row's API, and catalog axes are rejected because they stay rule-owned in the cascade block. Explicit `thinking`/`compat` on a seed row win over the cascade exactly as they do for any authored spec.
|
|
405
408
|
|
|
406
409
|
`bundle` decides when the generator includes the rows:
|
|
407
410
|
|
|
408
|
-
| Policy
|
|
409
|
-
|
|
|
410
|
-
| `always`
|
|
411
|
+
| Policy | Rows enter the bundle |
|
|
412
|
+
| ---------- | ------------------------------------------------------------------------- |
|
|
413
|
+
| `always` | Every regeneration. Same-id upstream/discovery rows win dedup. |
|
|
411
414
|
| `fallback` | Only when the provider's authoritative catalog discovery did not succeed. |
|
|
412
|
-
| `empty`
|
|
415
|
+
| `empty` | Only when no other source produced a row for the provider. |
|
|
413
416
|
|
|
414
417
|
`precedence="seed"` prepends the rows after the previous-snapshot merge and cross-provider reference fills, so the authored row wins dedup and same-id rows on other hosts never overwrite its name or capabilities (QwenCloud Token Plan, Meta). The default `upstream` precedence appends before the snapshot merge, so the current seed — not a stale snapshot copy — is the fallback row.
|
|
415
418
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
// TypeSafe (System One judgments)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// No `providers/typesafe.kdl` entry exists on purpose — it has no chat models,
|
|
5
|
-
// so it never appears in the model picker; the credential alone is what the
|
|
6
|
-
// judgment resolver checks.
|
|
1
|
+
// TypeSafe (System One judgments) answers typed questions over JSON state via
|
|
2
|
+
// `POST /v1/systemone`. Its judgment models are catalogued under the `judge`
|
|
3
|
+
// kind; this auth entry supplies both live `/v1/models` discovery and requests.
|
|
7
4
|
auth "typesafe" {
|
|
8
5
|
name "TypeSafe"
|
|
9
6
|
env "TYPESAFE_API_KEY"
|
|
@@ -33,11 +33,12 @@ class "qwen" {
|
|
|
33
33
|
}
|
|
34
34
|
// Qwen 3.8+ open-weight chat templates steer thinking depth through the
|
|
35
35
|
// `reasoning_effort` template kwarg (wire-exact low/medium/xhigh, template
|
|
36
|
-
// default xhigh) and raise on `enable_thinking: false
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// `enable_thinking`, so it rides the
|
|
40
|
-
|
|
36
|
+
// default xhigh) and raise on `enable_thinking: false`. LM Studio routes
|
|
37
|
+
// the selected effort onto the template instead of always reasoning at
|
|
38
|
+
// xhigh; llama.cpp owns its equivalent policy in the provider rule. vLLM
|
|
39
|
+
// ignores top-level `enable_thinking`, so it rides the
|
|
40
|
+
// chat-template-kwargs dialect.
|
|
41
|
+
on "lm-studio" {
|
|
41
42
|
revision ">=3.8" {
|
|
42
43
|
supports-reasoning-effort #true
|
|
43
44
|
template-reasoning-effort #true
|
|
@@ -4,6 +4,7 @@ provider "anthropic" {
|
|
|
4
4
|
default-model "claude-opus-4-8"
|
|
5
5
|
env "ANTHROPIC_API_KEY"
|
|
6
6
|
discovery label="Anthropic"
|
|
7
|
+
web-search "anthropic"
|
|
7
8
|
|
|
8
9
|
// Curated Anthropic models that are live or limited-availability on the
|
|
9
10
|
// first-party `/v1/models` endpoint but that stencil.so has not catalogued
|
|
@@ -5,4 +5,32 @@ provider "deepinfra" {
|
|
|
5
5
|
env "DEEPINFRA_API_KEY"
|
|
6
6
|
dynamic-models-authoritative #true
|
|
7
7
|
discovery label="DeepInfra" allow-unauthenticated=#true
|
|
8
|
+
kind-apis {
|
|
9
|
+
image "openai-images"
|
|
10
|
+
tts "openai-speech"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
seed base-url="https://api.deepinfra.com/v1/openai" bundle="always" {
|
|
14
|
+
model "black-forest-labs/FLUX-2-pro" name="FLUX.2 Pro" api="openai-images" {
|
|
15
|
+
reasoning #false
|
|
16
|
+
input "text" "image"
|
|
17
|
+
cost input=0 output=0 cache-read=0 cache-write=0
|
|
18
|
+
limits
|
|
19
|
+
supports-tools #false
|
|
20
|
+
}
|
|
21
|
+
model "hexgrad/Kokoro-82M" name="Kokoro-82M" api="openai-speech" {
|
|
22
|
+
reasoning #false
|
|
23
|
+
input "text"
|
|
24
|
+
cost input=0 output=0 cache-read=0 cache-write=0
|
|
25
|
+
limits
|
|
26
|
+
supports-tools #false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
models "black-forest-labs/FLUX-2-pro" {
|
|
31
|
+
kind "image"
|
|
32
|
+
}
|
|
33
|
+
models "hexgrad/Kokoro-82M" {
|
|
34
|
+
kind "tts"
|
|
35
|
+
}
|
|
8
36
|
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
provider "google-antigravity" {
|
|
4
4
|
default-model "gemini-3.1-pro"
|
|
5
|
+
kind-apis {
|
|
6
|
+
image "google-gemini-cli"
|
|
7
|
+
}
|
|
8
|
+
web-search "gemini"
|
|
9
|
+
|
|
10
|
+
seed api="google-gemini-cli" base-url="https://daily-cloudcode-pa.googleapis.com" bundle="always" {
|
|
11
|
+
model "gemini-3-pro-image" name="Gemini 3 Pro Image" {
|
|
12
|
+
reasoning #false
|
|
13
|
+
input "text" "image"
|
|
14
|
+
cost input=0 output=0 cache-read=0 cache-write=0
|
|
15
|
+
limits
|
|
16
|
+
supports-tools #false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
class "anthropic" {
|
|
7
21
|
family "opus" {
|
|
@@ -45,6 +59,9 @@ provider "google-antigravity" {
|
|
|
45
59
|
models "gemini-3-pro" {
|
|
46
60
|
thinking-mode "google-level"
|
|
47
61
|
}
|
|
62
|
+
models "gemini-3-pro-image" {
|
|
63
|
+
kind "image"
|
|
64
|
+
}
|
|
48
65
|
// residue: taxonomy ranks and exact globs do not isolate these models.
|
|
49
66
|
models "gemini-3.1-flash-lite" {
|
|
50
67
|
thinking-mode "google-level"
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
provider "google" {
|
|
4
4
|
default-model "gemini-3.1-pro-preview"
|
|
5
5
|
env "GEMINI_API_KEY"
|
|
6
|
+
kind-apis {
|
|
7
|
+
image "google-generative-ai"
|
|
8
|
+
}
|
|
9
|
+
web-search "gemini"
|
|
6
10
|
|
|
7
11
|
class "unknown" {
|
|
8
12
|
thinking-efforts "minimal" "low" "medium" "high"
|
|
@@ -17,4 +21,9 @@ provider "google" {
|
|
|
17
21
|
models "gemini-2.5-computer-use-preview-10-2025" "gemini-robotics-er-1.6-preview" {
|
|
18
22
|
thinking-efforts "minimal" "low" "medium" "high"
|
|
19
23
|
}
|
|
24
|
+
// The proxy conservatively classifies tool-capable text+image output as chat;
|
|
25
|
+
// this SKU is a native image generator on the Gemini image transport.
|
|
26
|
+
models "gemini-3.1-flash-lite-image" {
|
|
27
|
+
kind "image"
|
|
28
|
+
}
|
|
20
29
|
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
// Provider-wire compat for "llama.cpp".
|
|
2
2
|
|
|
3
3
|
provider "llama.cpp" {
|
|
4
|
+
supports-store #false
|
|
5
|
+
supports-developer-role #false
|
|
6
|
+
supports-reasoning-effort #false
|
|
4
7
|
// Replaces the string-only named-tool-choice provider table.
|
|
5
8
|
supports-named-tool-choice #false
|
|
9
|
+
|
|
10
|
+
class "qwen" {
|
|
11
|
+
// Responses cannot encode the template's thinking controls.
|
|
12
|
+
discovery-api "openai-completions"
|
|
13
|
+
// llama-server exposes the model's bundled chat template. Qwen templates
|
|
14
|
+
// preserve prior reasoning and accept the chat-template thinking controls.
|
|
15
|
+
thinking-upgrade-neutral #true
|
|
16
|
+
thinking-mode "effort"
|
|
17
|
+
thinking-efforts "low" "medium" "high" "max"
|
|
18
|
+
thinking-format "qwen"
|
|
19
|
+
reasoning-disable-mode "qwen-enable-thinking-false"
|
|
20
|
+
qwen-preserve-thinking #true
|
|
21
|
+
|
|
22
|
+
revision ">=3.8" {
|
|
23
|
+
supports-reasoning-effort #true
|
|
24
|
+
template-reasoning-effort #true
|
|
25
|
+
thinking-efforts "low" "medium" "xhigh"
|
|
26
|
+
thinking-requires-effort #true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
6
29
|
}
|