@oh-my-pi/pi-catalog 18.2.1 → 18.2.2

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.2] - 2026-09-16
6
+
7
+ ### Added
8
+
9
+ - OpenAI-compatible model discovery now fills in reasoning-effort tiers for unrecognized models using the shared catalog’s published reasoning options, while preserving explicit discovery metadata and reviewed model rules.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed recovery of corrupted model caches so private backups are preserved and concurrent recovery cannot overwrite a cache that has already been restored.
14
+ - Fixed pricing for Devin (SWE-2, SWE-1.7, and GLM-5.2 High) and Kimi Code models when upstream discovery omits cost information. SWE-2 now reflects its promotional pricing through December 31, 2026, then switches to list pricing on January 1, 2027.
15
+ - Fixed pricing and chat routing for Devin Fusion models so composite models use their own headline rates and supported Fusion lanes connect directly instead of failing through an incompatible routing path.
16
+
5
17
  ## [18.2.1] - 2026-09-15
6
18
 
7
19
  ### Added
@@ -30,3 +30,13 @@ export declare function resolveCascade(target: ResolveTarget): ResolvedAxes;
30
30
  * mutable rule data. Bundled target lookups use {@link resolveCascade}.
31
31
  */
32
32
  export declare function resolveCascadeRules(cascade: CompiledCascade, target: ResolveTarget): ResolvedAxes;
33
+ /**
34
+ * Whether the effort ladder this target resolves to comes from a rule scoped
35
+ * to the model's identity (a recognized class, family, revision, or an explicit
36
+ * model selector), rather than a provider/api-wide or fallback unknown-class
37
+ * rule that any unrecognized id at that provider inherits.
38
+ *
39
+ * Discovery reads this to tell reviewed tiers apart from a blanket default, so
40
+ * catalog-published tiers can correct the latter and never the former.
41
+ */
42
+ export declare function hasModelScopedEffortsRule(target: ResolveTarget): boolean;
@@ -124,6 +124,14 @@ export declare function deriveThinkingPairFamilies<TSpec extends VariantSpecLike
124
124
  * unrelated carriers (GitHub Copilot `-1m` context variants) never match.
125
125
  */
126
126
  export declare function isCollapsedVariantSpec(spec: VariantSpecLike): boolean;
127
+ /**
128
+ * Whether a Cursor wire id names an extended tier that upstream serves only in
129
+ * max mode. The compiled taxonomy identifies `xhigh`/`extra-high`/`max`
130
+ * efforts and their optional service lanes. This is an inference, not an
131
+ * upstream marker: it is the only per-tier signal available for bundled rows
132
+ * and for routes live discovery never advertised.
133
+ */
134
+ export declare function isCursorMaxModeWireId(wireModelId: string): boolean;
127
135
  /**
128
136
  * Collapse a full mixed-provider list: per provider, the compiled reviewed table, Cursor's
129
137
  * conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
@@ -13,3 +13,13 @@ export interface ResolvedModelPolicy<TApi extends Api = Api> {
13
13
  * complete compat record, thinking metadata, and catalog-data corrections.
14
14
  */
15
15
  export declare function resolveModelPolicy<TApi extends Api>(spec: ModelSpec<TApi>): ResolvedModelPolicy<TApi>;
16
+ /**
17
+ * Whether reviewed rules know THIS model's effort ladder, as opposed to it
18
+ * inheriting a provider-wide default or {@link resolveThinkingPolicy} falling
19
+ * through to the neutral wire ladder.
20
+ *
21
+ * Discovery uses this to tell "omp knows this model's tiers" apart from "omp
22
+ * is guessing them", so catalog-published tiers can correct the guess without
23
+ * ever overriding reviewed knowledge.
24
+ */
25
+ export declare function hasModelScopedEffortLadder<TApi extends Api>(spec: ModelSpec<TApi>): boolean;
@@ -1013,6 +1013,16 @@ export interface Model<TApi extends Api = Api> {
1013
1013
  gitlabDuoWorkflowRootNamespaceId?: string;
1014
1014
  /** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
1015
1015
  cursorMaxMode?: boolean;
1016
+ /**
1017
+ * Per-wire-id `max_mode` markers for the members a collapsed Cursor row
1018
+ * routes to, recorded by `collapseVariants` from live `GetUsableModels`
1019
+ * rows. {@link cursorMaxMode} on a collapsed row is an OR across members,
1020
+ * so it cannot tell a `-low` route that needs no max mode from an Opus
1021
+ * `-fast` route that does; transports look the routed wire id up here
1022
+ * first. Absent on raw rows (their own `cursorMaxMode` already describes
1023
+ * their single wire id) and on bundled snapshots that predate discovery.
1024
+ */
1025
+ cursorMaxModeRoutes?: Readonly<Record<string, boolean>>;
1016
1026
  cost: ModelCost;
1017
1027
  /** Premium Copilot requests charged per user-initiated request (defaults to 1). */
1018
1028
  premiumMultiplier?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "18.2.1",
4
+ "version": "18.2.2",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,12 +35,12 @@
35
35
  "gen:proto": "bun scripts/generate-protocols.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/omptype": "18.2.1",
39
- "@oh-my-pi/pi-utils": "18.2.1"
38
+ "@oh-my-pi/omptype": "18.2.2",
39
+ "@oh-my-pi/pi-utils": "18.2.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@bgotink/kdl": "0.4.0",
43
- "@oh-my-pi/pi-ai": "18.2.1",
43
+ "@oh-my-pi/pi-ai": "18.2.2",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
package/src/build.ts CHANGED
@@ -24,6 +24,41 @@ function objectPayload(value: unknown): object | undefined {
24
24
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
25
25
  }
26
26
 
27
+ /**
28
+ * Overwrite seeded fallback rates with the latest dated card whose
29
+ * `effectiveFrom` is already due. Leaves the seed unchanged when no card
30
+ * has started, and never attaches a `timeBased` tariff.
31
+ */
32
+ function applyEffectiveFallbackRates(
33
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number },
34
+ effectiveRates: unknown,
35
+ now = Date.now(),
36
+ ): void {
37
+ const rates = objectPayload(effectiveRates);
38
+ if (rates === undefined) return;
39
+ let latestFrom = Number.NEGATIVE_INFINITY;
40
+ let latest: object | undefined;
41
+ for (const entry of Object.values(rates)) {
42
+ const payload = objectPayload(entry);
43
+ if (payload === undefined) continue;
44
+ const date = Reflect.get(payload, "effectiveFrom");
45
+ if (typeof date !== "string") continue;
46
+ const from = Date.parse(date);
47
+ if (!Number.isFinite(from) || from > now || from < latestFrom) continue;
48
+ latestFrom = from;
49
+ latest = payload;
50
+ }
51
+ if (latest === undefined) return;
52
+ const input = numberField(latest, "input");
53
+ if (input !== undefined) cost.input = input;
54
+ const output = numberField(latest, "output");
55
+ if (output !== undefined) cost.output = output;
56
+ const cacheRead = numberField(latest, "cacheRead");
57
+ if (cacheRead !== undefined) cost.cacheRead = cacheRead;
58
+ const cacheWrite = numberField(latest, "cacheWrite");
59
+ if (cacheWrite !== undefined) cost.cacheWrite = cacheWrite;
60
+ }
61
+
27
62
  /** Narrow a compiled `input-modalities` axis value to the model input union. */
28
63
  function isInputModalities(value: unknown): value is ("text" | "image")[] {
29
64
  return Array.isArray(value) && value.every(entry => entry === "text" || entry === "image");
@@ -139,6 +174,30 @@ export function applyCatalogCorrections(
139
174
  const cacheWrite = numberField(patch, "cacheWrite");
140
175
  if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
141
176
  }
177
+ const fallback = objectPayload(catalog.costFallback);
178
+ if (fallback !== undefined) {
179
+ const base = model.cost;
180
+ const hasTokenPrice = base.input !== 0 || base.output !== 0 || base.cacheRead !== 0 || base.cacheWrite !== 0;
181
+ if (!hasTokenPrice) {
182
+ // Upstream reported no token price (plan-included or promo-free
183
+ // rows): seed the reviewed list price instead of overwriting real
184
+ // discovery data the way `cost-patch` would.
185
+ model.cost = { ...model.cost };
186
+ const input = numberField(fallback, "input");
187
+ if (input !== undefined) model.cost.input = input;
188
+ const output = numberField(fallback, "output");
189
+ if (output !== undefined) model.cost.output = output;
190
+ const cacheRead = numberField(fallback, "cacheRead");
191
+ if (cacheRead !== undefined) model.cost.cacheRead = cacheRead;
192
+ const cacheWrite = numberField(fallback, "cacheWrite");
193
+ if (cacheWrite !== undefined) model.cost.cacheWrite = cacheWrite;
194
+ // Dated fallback rates overwrite the seeded numbers when they have
195
+ // already taken effect. They are not a recurring tariff: wrapping
196
+ // them in `timeBased` with empty peak windows would report
197
+ // permanent off-peak and never wake at the dated boundary.
198
+ applyEffectiveFallbackRates(model.cost, Reflect.get(fallback, "effectiveRates"));
199
+ }
200
+ }
142
201
  if (catalog.timeBased !== undefined) {
143
202
  model.cost = { ...model.cost, timeBased: materializeTimeBasedCost(catalog.timeBased) };
144
203
  }
@@ -288,6 +288,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
288
288
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
289
289
  "context-window-floor": { key: "contextWindowFloor", set: "catalog", shape: "scalar" },
290
290
  "cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
291
+ "cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
291
292
  "delegation-bias": { key: "delegationBias", set: "catalog", shape: "scalar", values: DELEGATION_BIASES },
292
293
  "edit-prompt-variant": { key: "editPromptVariant", set: "catalog", shape: "scalar", values: ["full", "compact"] },
293
294
  "edit-revision": { key: "editRevision", set: "catalog", shape: "scalar" },
@@ -325,6 +325,30 @@ export function resolveCascadeRules(cascade: CompiledCascade, target: ResolveTar
325
325
  return cloneAxes(resolveOverIndex(buildRuleIndex(cascade), target));
326
326
  }
327
327
 
328
+ /**
329
+ * Whether the effort ladder this target resolves to comes from a rule scoped
330
+ * to the model's identity (a recognized class, family, revision, or an explicit
331
+ * model selector), rather than a provider/api-wide or fallback unknown-class
332
+ * rule that any unrecognized id at that provider inherits.
333
+ *
334
+ * Discovery reads this to tell reviewed tiers apart from a blanket default, so
335
+ * catalog-published tiers can correct the latter and never the former.
336
+ */
337
+ export function hasModelScopedEffortsRule(target: ResolveTarget): boolean {
338
+ const winners: WinnerTable = {};
339
+ for (const { rule, rank } of rankRelevantRules(getRuleIndex(), prepareTarget(target))) {
340
+ contest(winners, rule.compiled.thinking, rank, rule, target);
341
+ }
342
+ const winner = winners.efforts?.rule.compiled;
343
+ if (winner === undefined) return false;
344
+ return (
345
+ (winner.class !== undefined && winner.class !== "unknown") ||
346
+ winner.family !== undefined ||
347
+ winner.revision !== undefined ||
348
+ winner.models !== undefined
349
+ );
350
+ }
351
+
328
352
  function resolveOverIndex(index: RuleIndex, target: ResolveTarget): ResolvedAxes {
329
353
  const ranked = rankRelevantRules(index, prepareTarget(target));
330
354
  let reasoning = target.reasoning === true;
@@ -734,6 +734,80 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
734
734
  return spec;
735
735
  }
736
736
 
737
+ /**
738
+ * Whether a Cursor wire id names an extended tier that upstream serves only in
739
+ * max mode. The compiled taxonomy identifies `xhigh`/`extra-high`/`max`
740
+ * efforts and their optional service lanes. This is an inference, not an
741
+ * upstream marker: it is the only per-tier signal available for bundled rows
742
+ * and for routes live discovery never advertised.
743
+ */
744
+ export function isCursorMaxModeWireId(wireModelId: string): boolean {
745
+ const effort = collapseVariantId("cursor", wireModelId).effort;
746
+ return effort === Effort.XHigh || effort === Effort.Max;
747
+ }
748
+
749
+ /**
750
+ * Recover Cursor's max-mode marker for a bundled collapsed row. The bundled
751
+ * snapshot may contain only the logical row, but its effort routing still
752
+ * records the wire ids, so {@link isCursorMaxModeWireId} preserves the
753
+ * transport invariant even when live discovery contributes no raw members.
754
+ */
755
+ function reconcileCursorMaxModeFromRouting<TSpec extends VariantSpecLike>(spec: TSpec): TSpec {
756
+ if (spec.provider !== "cursor" || spec.cursorMaxMode === true) return spec;
757
+ const routing = spec.thinking?.effortRouting;
758
+ if (routing === undefined) return spec;
759
+ const hasMaxModeRoute = Object.values(routing).some(
760
+ (target): target is string => typeof target === "string" && isCursorMaxModeWireId(target),
761
+ );
762
+ return hasMaxModeRoute ? { ...spec, cursorMaxMode: true } : spec;
763
+ }
764
+
765
+ /**
766
+ * Index the discovered `max_mode` marker of every live Cursor member by its own
767
+ * wire id. The collapsed row's `cursorMaxMode` is an OR across members, so it
768
+ * says nothing per tier; the transport needs the marker addressable by the wire
769
+ * id it actually sends, because upstream marks tiers the slug cannot identify
770
+ * (the whole Opus `-fast` lane is max-mode, `-low-fast` included). Returns
771
+ * `undefined` when no member carries a marker, so unmarked rosters add no field.
772
+ */
773
+ function cursorMaxModeRoutesOf<TSpec extends VariantSpecLike>(
774
+ provider: string,
775
+ memberSpecs: readonly TSpec[],
776
+ ): Record<string, boolean> | undefined {
777
+ if (provider !== "cursor") return undefined;
778
+ let routes: Record<string, boolean> | undefined;
779
+ for (const member of memberSpecs) {
780
+ if (member.cursorMaxMode === undefined) continue;
781
+ routes ??= {};
782
+ routes[member.id] = member.cursorMaxMode;
783
+ }
784
+ return routes;
785
+ }
786
+
787
+ /**
788
+ * Lift Cursor's max-mode markers from live member rows onto an already-collapsed
789
+ * snapshot. Bundled catalog and cache rows froze the flag from `memberSpecs[0]`
790
+ * — the `-none`/`-low` tier — so the committed `gpt-5.6-*` / `cursor-grok-*`
791
+ * rows carry `cursorMaxMode: false`. The existing-collapsed pass-through keeps
792
+ * the snapshot verbatim, so a live `GetUsableModels` roster that marks the
793
+ * `-xhigh`/`-max` tiers would be discarded and max-tier requests would keep
794
+ * sending `max_mode: false` on a max-mode-only wire id. The row-level flag
795
+ * mirrors the fresh-collapse aggregation: only the positive case is lifted, so
796
+ * a roster that marks nothing leaves the snapshot alone. The per-wire-id
797
+ * markers are merged on top of the snapshot's own so live rows win per route.
798
+ * Returns `spec` by reference when unchanged.
799
+ */
800
+ function reconcileCursorMaxMode<TSpec extends VariantSpecLike>(spec: TSpec, memberSpecs: readonly TSpec[]): TSpec {
801
+ const routes = cursorMaxModeRoutesOf(spec.provider, memberSpecs);
802
+ const lifts = spec.cursorMaxMode !== true && memberSpecs.some(member => member.cursorMaxMode === true);
803
+ if (routes === undefined && !lifts) return spec;
804
+ return {
805
+ ...spec,
806
+ ...(lifts ? { cursorMaxMode: true } : {}),
807
+ ...(routes === undefined ? {} : { cursorMaxModeRoutes: { ...spec.cursorMaxModeRoutes, ...routes } }),
808
+ };
809
+ }
810
+
737
811
  /**
738
812
  * Collapse every family in `table` found in `specs`. Non-member specs pass
739
813
  * through verbatim (by reference), order preserved; the collapsed spec
@@ -776,7 +850,9 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
776
850
  // Recycled extraAliases rows are healed in a later pass.
777
851
  const refreshed =
778
852
  existing !== undefined && existingCollapsed
779
- ? reconcileDefaultMember(refreshCollapsedThinking(reconciled ?? existing, family, retired), family)
853
+ ? reconcileCursorMaxModeFromRouting(
854
+ reconcileDefaultMember(refreshCollapsedThinking(reconciled ?? existing, family, retired), family),
855
+ )
780
856
  : reconciled;
781
857
  if (refreshed !== undefined && refreshed !== existing) {
782
858
  familyIdBySpecId.set(family.id, family.id);
@@ -788,20 +864,26 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
788
864
  for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
789
865
  if (existing) familyIdBySpecId.set(family.id, family.id);
790
866
 
867
+ const memberSpecs: TSpec[] = [];
868
+ for (const id of rawPresent) {
869
+ const member = byId.get(id);
870
+ if (member !== undefined) memberSpecs.push(member);
871
+ }
872
+
791
873
  if (existingCollapsed && reconciled !== undefined) {
792
874
  // Mixed input: the collapsed entry wins; stale raw members are deduped
793
875
  // away. Retired targets are re-pointed first, then the default wire id
794
876
  // prefers the family's declared member when live and otherwise falls
795
- // back to the first member the account actually advertised.
796
- replacement.set(family.id, reconcileDefaultMember(reconciled, family, new Set(rawPresent)));
877
+ // back to the first member the account actually advertised. The live
878
+ // members still own `cursorMaxMode`: the snapshot froze it from the
879
+ // lowest tier.
880
+ replacement.set(
881
+ family.id,
882
+ reconcileCursorMaxMode(reconcileDefaultMember(reconciled, family, new Set(rawPresent)), memberSpecs),
883
+ );
797
884
  continue;
798
885
  }
799
886
 
800
- const memberSpecs: TSpec[] = [];
801
- for (const id of rawPresent) {
802
- const member = byId.get(id);
803
- if (member !== undefined) memberSpecs.push(member);
804
- }
805
887
  const firstMember = memberSpecs[0];
806
888
  if (firstMember === undefined) continue;
807
889
  const presentSet = new Set(rawPresent);
@@ -836,6 +918,14 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
836
918
  if (memberSpecs.some(spec => spec.input.includes("text"))) input.push("text");
837
919
  if (memberSpecs.some(spec => spec.input.includes("image"))) input.push("image");
838
920
 
921
+ // `cursorMaxMode` gates the `max_mode` request flag. The collapsed row
922
+ // otherwise inherits `memberSpecs[0]`, so a family whose max-mode member
923
+ // is not the first one would advertise `false` and send `max_mode: false`
924
+ // on a max-mode wire id. Only the positive case is aggregated — an
925
+ // unmarked family keeps whatever the first member carried. The OR loses
926
+ // which tier needed it, so the members' own markers are kept per wire id.
927
+ const cursorMaxMode = memberSpecs.some(spec => spec.cursorMaxMode === true) ? true : undefined;
928
+ const cursorMaxModeRoutes = cursorMaxModeRoutesOf(firstMember.provider, memberSpecs);
839
929
  const collapsed: TSpec = {
840
930
  ...firstMember,
841
931
  id: family.id,
@@ -844,6 +934,8 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
844
934
  input,
845
935
  contextWindow: maxOrNull(memberSpecs.map(spec => spec.contextWindow)),
846
936
  maxTokens: maxOrNull(memberSpecs.map(spec => spec.maxTokens)),
937
+ ...(cursorMaxMode === undefined ? {} : { cursorMaxMode }),
938
+ ...(cursorMaxModeRoutes === undefined ? {} : { cursorMaxModeRoutes }),
847
939
  };
848
940
  // The default wire id is the family's declared `defaultMember` when live,
849
941
  // else the highest-priority live member. Omitted when it equals the
@@ -34,7 +34,7 @@ import type {
34
34
  import { isAnthropicSigningProxyUrl, isAzureAnthropicRoute, isOfficialAnthropicApiUrl } from "./anthropic";
35
35
  import { applyCompatOverrides } from "./apply";
36
36
  import { API_COMPAT_RECORDS, AXES, type CompatRecordName } from "./axes";
37
- import { resolveCascade } from "./cascade";
37
+ import { hasModelScopedEffortsRule, resolveCascade } from "./cascade";
38
38
  import { compareRevision, parseRevision, type Revision } from "./revision";
39
39
  import { classifyModel, stripThinkingVariantSuffix } from "./taxonomy";
40
40
  import type { ModelIdentity, ResolvedAxes, ResolveTarget } from "./types";
@@ -1271,3 +1271,16 @@ export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Ap
1271
1271
  catalog: axes.catalog,
1272
1272
  };
1273
1273
  }
1274
+
1275
+ /**
1276
+ * Whether reviewed rules know THIS model's effort ladder, as opposed to it
1277
+ * inheriting a provider-wide default or {@link resolveThinkingPolicy} falling
1278
+ * through to the neutral wire ladder.
1279
+ *
1280
+ * Discovery uses this to tell "omp knows this model's tiers" apart from "omp
1281
+ * is guessing them", so catalog-published tiers can correct the guess without
1282
+ * ever overriding reviewed knowledge.
1283
+ */
1284
+ export function hasModelScopedEffortLadder<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
1285
+ return hasModelScopedEffortsRule(buildResolveTarget(spec, resolveIdentity(spec)));
1286
+ }
@@ -39,4 +39,50 @@ provider "devin" {
39
39
  supports-parallel-tool-calls #true
40
40
  }
41
41
  }
42
+
43
+
44
+ // Devin omits cost dimensions for plan-included models on lower tiers
45
+ // (PRO reports $0 for SWE-2, SWE-1.7, and GLM-5.2 High). `cost-fallback`
46
+ // seeds the enterprise-tier list price only when upstream reports no
47
+ // token price, so accounts whose discovery does carry dimensions keep
48
+ // their real rates.
49
+
50
+ // SWE-2: enterprise promo is 75% off list through 2026-12-31; list
51
+ // pricing ($3/$15, $0.30 cache read) applies from 2027-01-01.
52
+ models "swe-2*" {
53
+ cost-fallback {
54
+ input 0.75
55
+ output 3.75
56
+ cache-read 0.075
57
+ cache-write 0.75
58
+ effective-rates {
59
+ list-price {
60
+ effective-from "2027-01-01T00:00:00Z"
61
+ input 3
62
+ output 15
63
+ cache-read 0.3
64
+ cache-write 3
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ // SWE-1.7 and GLM-5.2 High: plan-included on PRO, no published promo
71
+ // schedule — enterprise list is the only published per-token price.
72
+ models "swe-1-7" "swe-1-7-medium" {
73
+ cost-fallback {
74
+ input 0.5
75
+ output 2.5
76
+ cache-read 0.2
77
+ cache-write 0.5
78
+ }
79
+ }
80
+ models "glm-5-2" {
81
+ cost-fallback {
82
+ input 1.4
83
+ output 4.4
84
+ cache-read 0.26
85
+ cache-write 1.4
86
+ }
87
+ }
42
88
  }
@@ -39,4 +39,52 @@ provider "kimi-code" {
39
39
  thinking-efforts "minimal" "low" "medium" "high"
40
40
  thinking-format "zai"
41
41
  }
42
+
43
+ // /coding/v1/models carries no pricing. `cost-fallback` seeds the public
44
+ // Moonshot list price only when discovery reports $0, so a future priced
45
+ // envelope keeps its real rates. `kimi-for-coding` is K2.8 Preview — no
46
+ // public API SKU exists yet, so it uses the K2.7 Code line rate.
47
+ models "k3" "k3-256k" {
48
+ cost-fallback {
49
+ input 3.0
50
+ output 15.0
51
+ cache-read 0.3
52
+ }
53
+ }
54
+ models "kimi-for-coding" {
55
+ cost-fallback {
56
+ input 0.95
57
+ output 4.0
58
+ cache-read 0.19
59
+ }
60
+ }
61
+ models "kimi-for-coding-highspeed" {
62
+ cost-fallback {
63
+ input 1.9
64
+ output 8.0
65
+ cache-read 0.38
66
+ }
67
+ }
68
+ // Moonshot K2 list (`kimi-k2-0711-preview` / `kimi-k2-0905-preview`), not K2.6.
69
+ models "kimi-k2" {
70
+ cost-fallback {
71
+ input 0.6
72
+ output 2.5
73
+ cache-read 0.15
74
+ }
75
+ }
76
+ models "kimi-k2.5" {
77
+ cost-fallback {
78
+ input 0.6
79
+ output 3.0
80
+ cache-read 0.1
81
+ }
82
+ }
83
+ models "kimi-k2-turbo-preview" {
84
+ cost-fallback {
85
+ input 2.4
86
+ output 10.0
87
+ cache-read 0.6
88
+ }
89
+ }
42
90
  }