@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +24 -3
  2. package/README.md +18 -18
  3. package/THIRD-PARTY-NOTICES.txt +0 -37
  4. package/dist/types/build.d.ts +7 -1
  5. package/dist/types/compat/auth-ids.d.ts +1 -1
  6. package/dist/types/compat/axes.d.ts +1 -1
  7. package/dist/types/compat/cascade.d.ts +3 -2
  8. package/dist/types/compat/provider-ids.d.ts +1 -1
  9. package/dist/types/compat/resolve.d.ts +2 -0
  10. package/dist/types/compat/taxonomy.d.ts +2 -2
  11. package/dist/types/compat/types.d.ts +18 -7
  12. package/dist/types/discovery/index.d.ts +1 -0
  13. package/dist/types/discovery/openai-compatible.d.ts +2 -0
  14. package/dist/types/discovery/typesafe.d.ts +26 -0
  15. package/dist/types/model-manager.d.ts +1 -1
  16. package/dist/types/provider-models/openai-compat.d.ts +6 -4
  17. package/dist/types/provider-models/special.d.ts +10 -0
  18. package/dist/types/types.d.ts +23 -0
  19. package/package.json +38 -38
  20. package/src/build.ts +27 -5
  21. package/src/compat/auth-ids.ts +2 -0
  22. package/src/compat/axes.ts +9 -1
  23. package/src/compat/cascade.ts +45 -23
  24. package/src/compat/provider-ids.ts +3 -0
  25. package/src/compat/resolve.ts +24 -14
  26. package/src/compat/rules/README.md +38 -35
  27. package/src/compat/rules/auth/local.kdl +4 -0
  28. package/src/compat/rules/auth/typesafe.kdl +3 -6
  29. package/src/compat/rules/auth/web.kdl +4 -0
  30. package/src/compat/rules/classes/qwen.kdl +6 -5
  31. package/src/compat/rules/providers/anthropic.kdl +1 -0
  32. package/src/compat/rules/providers/deepinfra.kdl +28 -0
  33. package/src/compat/rules/providers/google-antigravity.kdl +17 -0
  34. package/src/compat/rules/providers/google.kdl +9 -0
  35. package/src/compat/rules/providers/llama.cpp.kdl +23 -0
  36. package/src/compat/rules/providers/local.kdl +110 -0
  37. package/src/compat/rules/providers/openai-codex.kdl +18 -0
  38. package/src/compat/rules/providers/openai.kdl +61 -0
  39. package/src/compat/rules/providers/openrouter.kdl +132 -0
  40. package/src/compat/rules/providers/typesafe.kdl +21 -0
  41. package/src/compat/rules/providers/web.kdl +153 -0
  42. package/src/compat/rules/providers/xai-oauth.kdl +26 -0
  43. package/src/compat/rules/providers/xai.kdl +22 -0
  44. package/src/compat/rules/runtime/behavior.kdl +2 -1
  45. package/src/compat/rules/taxonomy/qwen.kdl +2 -0
  46. package/src/compat/rules.json +12526 -1
  47. package/src/compat/taxonomy.ts +92 -31
  48. package/src/compat/types.ts +22 -7
  49. package/src/discovery/devin.ts +3 -1
  50. package/src/discovery/index.ts +1 -0
  51. package/src/discovery/openai-compatible.ts +4 -1
  52. package/src/discovery/typesafe.ts +118 -0
  53. package/src/model-manager.ts +44 -15
  54. package/src/models.json +1 -1
  55. package/src/provider-models/descriptors.ts +6 -0
  56. package/src/provider-models/openai-compat.ts +357 -78
  57. package/src/provider-models/special.ts +53 -0
  58. 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 { Api, Model, ModelSpec } from "./types";
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
- // An exact `thinking-efforts` rule upgrades a stale `reasoning: false`
287
- // discovery default (see `resolveThinkingPolicy`); materialize the
288
- // correction so transports and the picker see a reasoning-capable model.
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,
@@ -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"
@@ -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 { ThinkingControlMode } from "../types";
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" },
@@ -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 can enable reasoning; absent family/revision
318
- * facts never satisfy selectors that require them. Returned axes are caller-owned.
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
- if (rule.hasExactEffortsRule && rank[0] === 2) {
373
- reasoning = true;
374
- break;
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"
@@ -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[provider] !== true &&
325
- (LOCAL_OPENAI_COMPAT_PROVIDERS[provider] === true || hasLocalLoopbackBaseUrl(baseUrl));
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
- qwenTemplateReasoningEffort:
525
- (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") &&
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[provider] !== true && LOCAL_OPENAI_COMPAT_PROVIDERS[provider] === true) ||
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
- * any exact `thinking-efforts` rule (the cascade's thinking-axis gate), but
1067
- * compat may only be repaired where the provider contract opted in with
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 an exact `thinking-efforts` ladder (the cascade upgrade for
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>(spec: ModelSpec<TApi>, identity: ModelIdentity): ResolveTarget {
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: spec.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 | 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. |
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 model identifier, compared case-insensitively), `rationale`, and `provenance`.
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 | 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. |
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 pair `(provider, model)` must also be unique, including provider-agnostic pairs. When no observation time is supplied, an expiring override remains active.
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 | Form | Matching semantics |
182
- | --- | --- | --- |
183
- | `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`. |
184
- | `provider` | `provider "id" { ... }` | Exact provider ID. It is root-only and may contain `class` and `models`. |
185
- | `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`. |
186
- | `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`. |
187
- | `family` | `family "id" { ... }` | Exact classified family ID. It may contain `revision` and `models`. A target with no family does not match. |
188
- | `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. |
189
- | `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. |
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 are considered only when the structured resolve target sets `reasoning` — except that an exact model selector declaring `thinking-efforts` upgrades the target (a reviewed correction to stale source capability metadata). 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.
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` *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.
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 | Rows enter the bundle |
409
- | --- | --- |
410
- | `always` | Every regeneration. Same-id upstream/discovery rows win dedup. |
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` | Only when no other source produced a row for the provider. |
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
 
@@ -0,0 +1,4 @@
1
+ auth "local" {
2
+ name "Local models"
3
+ allows-missing-api-key #true
4
+ }
@@ -1,9 +1,6 @@
1
- // TypeSafe (System One judgments) is not a chat-model provider: it answers
2
- // typed questions (choice / yes-no / score) over JSON state via
3
- // `POST /v1/systemone`, consumed through `@oh-my-pi/pi-ai`'s `TypeSafeJudge`.
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"
@@ -0,0 +1,4 @@
1
+ auth "web" {
2
+ name "Web search engines"
3
+ allows-missing-api-key #true
4
+ }
@@ -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`, so local
37
- // llama.cpp-style hosts route the selected effort onto the template instead
38
- // of always reasoning at xhigh. vLLM ignores top-level
39
- // `enable_thinking`, so it rides the chat-template-kwargs dialect.
40
- on "llama.cpp" "lm-studio" {
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
  }