@linxiraos/pi-catalog 1.1.13 → 1.1.15

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 (43) hide show
  1. package/CHANGELOG.md +2 -69
  2. package/README.md +20 -0
  3. package/dist/types/compat/auth-ids.d.ts +2 -2
  4. package/dist/types/models.d.ts +10 -6
  5. package/dist/types/pricing.d.ts +5 -0
  6. package/dist/types/provider-models/descriptor-types.d.ts +15 -0
  7. package/dist/types/provider-models/descriptors.d.ts +20 -0
  8. package/dist/types/provider-models/openai-compat.d.ts +44 -0
  9. package/dist/types/types.d.ts +43 -2
  10. package/dist/types/wire/charm-hyper.d.ts +21 -0
  11. package/dist/types/wire/github-copilot.d.ts +19 -0
  12. package/package.json +4 -4
  13. package/src/build.ts +16 -2
  14. package/src/compat/auth-ids.ts +4 -0
  15. package/src/compat/axes.ts +9 -0
  16. package/src/compat/resolve.ts +18 -4
  17. package/src/compat/rules/README.md +37 -0
  18. package/src/compat/rules/auth/_order.kdl +1 -1
  19. package/src/compat/rules/auth/charm-hyper.kdl +19 -0
  20. package/src/compat/rules/auth/commandcode.kdl +13 -0
  21. package/src/compat/rules/classes/anthropic.kdl +41 -0
  22. package/src/compat/rules/classes/deepseek.kdl +11 -0
  23. package/src/compat/rules/classes/minimax.kdl +4 -2
  24. package/src/compat/rules/providers/amazon-bedrock.kdl +7 -0
  25. package/src/compat/rules/providers/anthropic.kdl +8 -0
  26. package/src/compat/rules/providers/charm-hyper.kdl +82 -0
  27. package/src/compat/rules/providers/commandcode.kdl +601 -0
  28. package/src/compat/rules/providers/deepseek.kdl +83 -1
  29. package/src/compat/rules/providers/opencode-go.kdl +22 -0
  30. package/src/compat/rules/providers/opencode-zen.kdl +12 -0
  31. package/src/compat/rules/runtime/behavior.kdl +5 -0
  32. package/src/compat/rules.json +2143 -131
  33. package/src/model-manager.ts +27 -13
  34. package/src/models.json +12953 -5481
  35. package/src/models.ts +93 -19
  36. package/src/pricing.ts +136 -0
  37. package/src/provider-models/cache-provider-id.ts +28 -4
  38. package/src/provider-models/descriptor-types.ts +15 -0
  39. package/src/provider-models/descriptors.ts +36 -0
  40. package/src/provider-models/openai-compat.ts +297 -3
  41. package/src/types.ts +46 -1
  42. package/src/wire/charm-hyper.ts +26 -0
  43. package/src/wire/github-copilot.ts +26 -0
package/src/models.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { classifyModel } from "./compat/taxonomy";
2
2
  import MODELS from "./models.json" with { type: "json" };
3
- import type { Api, KnownProvider, Model, ModelCost, TokenCost, Usage } from "./types";
3
+ import type {
4
+ Api,
5
+ EffectiveTokenCost,
6
+ KnownProvider,
7
+ Model,
8
+ ModelCost,
9
+ TimeBasedCost,
10
+ TokenCost,
11
+ Usage,
12
+ } from "./types";
4
13
 
5
14
  /**
6
15
  * Static bundled model registry loaded from `models.json`.
@@ -48,38 +57,103 @@ export function getBundledModels(provider: GeneratedProvider): Model<Api>[] {
48
57
  const models = getProviderModels(provider);
49
58
  return models ? (Array.from(models.values()) as Model<Api>[]) : [];
50
59
  }
51
- function resolveTokenCost(cost: ModelCost, promptInputTokens: number): TokenCost {
52
- const longContext = cost.longContext;
53
- if (!longContext) return cost;
60
+ function resolveTokenCost(cost: ModelCost, promptInputTokens: number, timestamp: number | undefined): TokenCost {
61
+ let rates: ModelCost | EffectiveTokenCost = cost;
62
+ let effectiveFrom = -Infinity;
63
+ if (timestamp !== undefined && cost.timeBased?.effectiveRates) {
64
+ for (const candidate of cost.timeBased.effectiveRates) {
65
+ if (candidate.effectiveFrom <= timestamp && candidate.effectiveFrom > effectiveFrom) {
66
+ rates = candidate;
67
+ effectiveFrom = candidate.effectiveFrom;
68
+ }
69
+ }
70
+ }
71
+ const longContext = rates.longContext;
72
+ if (!longContext) return rates;
54
73
  const reachesThreshold =
55
74
  promptInputTokens > longContext.inputThreshold ||
56
75
  (longContext.inputThresholdInclusive === true && promptInputTokens === longContext.inputThreshold);
57
- return reachesThreshold ? longContext : cost;
76
+ return reachesThreshold ? longContext : rates;
77
+ }
78
+
79
+ function isPeakPricingPeriod(schedule: TimeBasedCost, timestamp: number): boolean {
80
+ // Unix epoch was Thursday. Arithmetic keeps this UTC-only without allocating a Date.
81
+ const day = Math.floor(timestamp / 86_400_000);
82
+ const weekday = (((day + 4) % 7) + 7) % 7;
83
+ const minute = Math.floor((timestamp - day * 86_400_000) / 60_000);
84
+ for (const window of schedule.peakWindows) {
85
+ if (minute >= window.startMinute && minute < window.endMinute && window.weekdays.includes(weekday)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ function timeBasedMultiplier(schedule: TimeBasedCost | undefined, timestamp: number | undefined): number {
91
+ if (!schedule || timestamp === undefined) return 1;
92
+ return isPeakPricingPeriod(schedule, timestamp) ? 1 : schedule.offPeakMultiplier;
93
+ }
94
+
95
+ /** Return the recurring UTC tariff period, independently of its monetary multiplier. */
96
+ export function getTimeBasedPricingPeriod(cost: ModelCost, timestamp?: number): "peak" | "off-peak" | undefined {
97
+ const schedule = cost.timeBased;
98
+ if (!schedule) return undefined;
99
+ return isPeakPricingPeriod(schedule, timestamp ?? Date.now()) ? "peak" : "off-peak";
100
+ }
101
+
102
+ /** Return the next actual peak/off-peak change strictly after the Unix-ms timestamp. */
103
+ export function getNextTimeBasedPricingTransition(cost: ModelCost, timestamp?: number): number | undefined {
104
+ const schedule = cost.timeBased;
105
+ if (!schedule) return undefined;
106
+ const now = timestamp ?? Date.now();
107
+ const firstDay = Math.floor(now / 86_400_000);
108
+ const horizon = now + 7 * 86_400_000;
109
+ let next = Infinity;
110
+ // All changes occur at window edges; one UTC week covers the entire recurrence.
111
+ for (let offset = 0; offset <= 7; offset++) {
112
+ const day = firstDay + offset;
113
+ const weekday = (((day + 4) % 7) + 7) % 7;
114
+ for (const window of schedule.peakWindows) {
115
+ if (!window.weekdays.includes(weekday)) continue;
116
+ for (let edge = 0; edge < 2; edge++) {
117
+ const minute = edge === 0 ? window.startMinute : window.endMinute;
118
+ const candidate = day * 86_400_000 + minute * 60_000;
119
+ if (candidate <= now || candidate > horizon || candidate >= next) continue;
120
+ // Overlapping/touching windows can hide an edge, including at midnight.
121
+ if (isPeakPricingPeriod(schedule, candidate - 1) !== isPeakPricingPeriod(schedule, candidate)) {
122
+ next = candidate;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return next === Infinity ? undefined : next;
58
128
  }
59
129
 
60
- /** Price a prompt as fully uncached input under its active context-length tier. */
61
- export function calculateUncachedInputCost(cost: ModelCost, promptInputTokens: number): number {
62
- const rates = resolveTokenCost(cost, promptInputTokens);
63
- return (rates.input / 1_000_000) * promptInputTokens;
130
+ /** Price a fully uncached prompt at its request timestamp (Unix ms); scheduled prices default to now. */
131
+ export function calculateUncachedInputCost(cost: ModelCost, promptInputTokens: number, timestamp?: number): number {
132
+ const pricingTimestamp = cost.timeBased ? (timestamp ?? Date.now()) : undefined;
133
+ const rates = resolveTokenCost(cost, promptInputTokens, pricingTimestamp);
134
+ return (rates.input / 1_000_000) * promptInputTokens * timeBasedMultiplier(cost.timeBased, pricingTimestamp);
64
135
  }
65
136
 
66
- /** Price one usage record from a token rate card, including active context tiers. */
67
- export function calculateUsageCost(cost: ModelCost, usage: Usage): Usage["cost"] {
137
+ /** Price usage at its request timestamp (Unix ms); only scheduled prices default to now. */
138
+ export function calculateUsageCost(cost: ModelCost, usage: Usage, timestamp?: number): Usage["cost"] {
68
139
  const orchestration = usage.orchestration;
69
140
  const promptInputTokens =
70
141
  usage.input + usage.cacheRead + usage.cacheWrite + (orchestration?.input ?? 0) + (orchestration?.cacheRead ?? 0);
71
- const rates = resolveTokenCost(cost, promptInputTokens);
72
- usage.cost.input = (rates.input / 1000000) * (usage.input + (orchestration?.input ?? 0));
73
- usage.cost.output = (rates.output / 1000000) * (usage.output + (orchestration?.output ?? 0));
74
- usage.cost.cacheRead = (rates.cacheRead / 1000000) * (usage.cacheRead + (orchestration?.cacheRead ?? 0));
75
- usage.cost.cacheWrite = cacheWriteCost(rates, usage);
142
+ const pricingTimestamp = cost.timeBased ? (timestamp ?? Date.now()) : undefined;
143
+ const rates = resolveTokenCost(cost, promptInputTokens, pricingTimestamp);
144
+ const multiplier = timeBasedMultiplier(cost.timeBased, pricingTimestamp);
145
+ usage.cost.input = (rates.input / 1000000) * (usage.input + (orchestration?.input ?? 0)) * multiplier;
146
+ usage.cost.output = (rates.output / 1000000) * (usage.output + (orchestration?.output ?? 0)) * multiplier;
147
+ usage.cost.cacheRead =
148
+ (rates.cacheRead / 1000000) * (usage.cacheRead + (orchestration?.cacheRead ?? 0)) * multiplier;
149
+ usage.cost.cacheWrite = cacheWriteCost(rates, usage) * multiplier;
76
150
  usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
77
151
  return usage.cost;
78
152
  }
79
153
 
80
- /** Price one usage record from its model's token rate card. */
81
- export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
82
- return calculateUsageCost(model.cost, usage);
154
+ /** Price usage at its request timestamp (Unix ms); preserve the resulting monetary amounts for display. */
155
+ export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage, timestamp?: number): Usage["cost"] {
156
+ return calculateUsageCost(model.cost, usage, timestamp);
83
157
  }
84
158
 
85
159
  /**
package/src/pricing.ts ADDED
@@ -0,0 +1,136 @@
1
+ import type { EffectiveTokenCost, LongContextTokenCost, PeakPricingWindow, TimeBasedCost, TokenCost } from "./types";
2
+ import { isRecord } from "./utils";
3
+
4
+ function nonnegative(value: unknown): value is number {
5
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
6
+ }
7
+
8
+ function isRates(value: unknown): value is TokenCost {
9
+ return (
10
+ isRecord(value) &&
11
+ nonnegative(value.input) &&
12
+ nonnegative(value.output) &&
13
+ nonnegative(value.cacheRead) &&
14
+ nonnegative(value.cacheWrite)
15
+ );
16
+ }
17
+
18
+ function isLongContext(value: unknown): value is LongContextTokenCost {
19
+ return (
20
+ isRecord(value) &&
21
+ isRates(value) &&
22
+ typeof value.inputThreshold === "number" &&
23
+ Number.isFinite(value.inputThreshold) &&
24
+ value.inputThreshold > 0 &&
25
+ (value.inputThresholdInclusive === undefined || typeof value.inputThresholdInclusive === "boolean")
26
+ );
27
+ }
28
+
29
+ function isPeakWindow(value: unknown): value is PeakPricingWindow {
30
+ return (
31
+ isRecord(value) &&
32
+ Array.isArray(value.weekdays) &&
33
+ value.weekdays.length > 0 &&
34
+ value.weekdays.every(day => Number.isInteger(day) && day >= 0 && day <= 6) &&
35
+ new Set(value.weekdays).size === value.weekdays.length &&
36
+ typeof value.startMinute === "number" &&
37
+ Number.isInteger(value.startMinute) &&
38
+ typeof value.endMinute === "number" &&
39
+ Number.isInteger(value.endMinute) &&
40
+ value.startMinute >= 0 &&
41
+ value.endMinute <= 1440 &&
42
+ value.startMinute < value.endMinute
43
+ );
44
+ }
45
+
46
+ function isEffectiveRate(value: unknown): value is EffectiveTokenCost {
47
+ return (
48
+ isRecord(value) &&
49
+ isRates(value) &&
50
+ typeof value.effectiveFrom === "number" &&
51
+ Number.isSafeInteger(value.effectiveFrom) &&
52
+ Math.abs(value.effectiveFrom) <= 8_640_000_000_000_000 &&
53
+ (value.longContext === undefined || isLongContext(value.longContext))
54
+ );
55
+ }
56
+
57
+ /** Validate serialized schedules before admitting cached model rows. */
58
+ export function isTimeBasedCost(value: unknown): value is TimeBasedCost {
59
+ if (
60
+ !isRecord(value) ||
61
+ !nonnegative(value.offPeakMultiplier) ||
62
+ !Array.isArray(value.peakWindows) ||
63
+ !value.peakWindows.every(isPeakWindow)
64
+ ) {
65
+ return false;
66
+ }
67
+ if (value.effectiveRates === undefined) return true;
68
+ if (!Array.isArray(value.effectiveRates) || !value.effectiveRates.every(isEffectiveRate)) return false;
69
+ const dates = new Set<number>();
70
+ for (const rate of value.effectiveRates) {
71
+ if (dates.has(rate.effectiveFrom)) return false;
72
+ dates.add(rate.effectiveFrom);
73
+ }
74
+ return true;
75
+ }
76
+
77
+ function payload(value: unknown, keys: readonly string[], field: string): Record<string, unknown> {
78
+ if (!isRecord(value) || Object.keys(value).some(key => !keys.includes(key))) {
79
+ throw new Error(`Invalid time-based-cost ${field}`);
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function namedEntries(value: unknown, field: string): unknown[] {
85
+ if (!isRecord(value)) throw new Error(`Invalid time-based-cost ${field}: expected named objects`);
86
+ return Object.values(value);
87
+ }
88
+
89
+ /** KDL object children are named records; normalize them once while constructing the model. */
90
+ export function materializeTimeBasedCost(value: unknown): TimeBasedCost {
91
+ const source = payload(value, ["offPeakMultiplier", "peakWindows", "effectiveRates"], "schedule");
92
+ const peakWindows = namedEntries(source.peakWindows, "peak-windows").map(entry => {
93
+ const window = payload(entry, ["weekdays", "startMinute", "endMinute"], "peak-window");
94
+ if (typeof window.weekdays !== "string" || !/^[0-6](,[0-6])*$/.test(window.weekdays)) {
95
+ throw new Error("Invalid time-based-cost weekdays: expected comma-separated UTC weekday numbers");
96
+ }
97
+ return { ...window, weekdays: window.weekdays.split(",").map(Number) };
98
+ });
99
+ const effectiveRates =
100
+ source.effectiveRates === undefined
101
+ ? undefined
102
+ : namedEntries(source.effectiveRates, "effective-rates").map(entry => {
103
+ const rate = payload(
104
+ entry,
105
+ ["effectiveFrom", "input", "output", "cacheRead", "cacheWrite", "longContext"],
106
+ "effective-rate",
107
+ );
108
+ const date = rate.effectiveFrom;
109
+ if (typeof date !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(date)) {
110
+ throw new Error("Invalid time-based-cost effective-from: expected an ISO UTC timestamp");
111
+ }
112
+ const effectiveFrom = Date.parse(date);
113
+ if (
114
+ !Number.isFinite(effectiveFrom) ||
115
+ new Date(effectiveFrom).toISOString() !== (date.includes(".") ? date : date.replace("Z", ".000Z"))
116
+ ) {
117
+ throw new Error("Invalid time-based-cost effective-from: invalid UTC date");
118
+ }
119
+ if (rate.longContext !== undefined) {
120
+ payload(
121
+ rate.longContext,
122
+ ["inputThreshold", "inputThresholdInclusive", "input", "output", "cacheRead", "cacheWrite"],
123
+ "long-context",
124
+ );
125
+ }
126
+ return { ...rate, effectiveFrom };
127
+ });
128
+ const schedule = {
129
+ offPeakMultiplier: source.offPeakMultiplier,
130
+ peakWindows,
131
+ ...(effectiveRates && { effectiveRates }),
132
+ };
133
+ if (!isTimeBasedCost(schedule))
134
+ throw new Error("Invalid time-based-cost schedule: invalid windows, rates, or multiplier");
135
+ return schedule;
136
+ }
@@ -1,3 +1,4 @@
1
+ import { CHARM_HYPER_API_BASE_URL, normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
1
2
  import { PERSONAL_GITHUB_COPILOT_BASE_URL } from "../wire/github-copilot";
2
3
 
3
4
  export interface ModelCacheProviderIdOptions {
@@ -19,6 +20,8 @@ export function isCredentialScopedModelCacheProvider(providerId: string): boolea
19
20
 
20
21
  export function getDefaultModelDiscoveryBaseUrl(providerId: string): string | undefined {
21
22
  switch (providerId) {
23
+ case "charm-hyper":
24
+ return CHARM_HYPER_API_BASE_URL;
22
25
  case "meta":
23
26
  case "muse-code":
24
27
  return "https://api.meta.ai/v1";
@@ -62,6 +65,23 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
62
65
  // carry `requestModelId: *-low`, which the Start plan refuses; refetch
63
66
  // so the collapsed default is re-pointed to `-medium` (issue #9478).
64
67
  return "cursor:default-effort-v4";
68
+ case "charm-hyper": {
69
+ // Discovery is authoritative for this gateway, so a warm cache is served
70
+ // for its full TTL without re-probing: the namespace must follow the
71
+ // configured endpoint, or a self-hosted proxy keeps serving the canonical
72
+ // host's roster, capabilities and tariffs until expiry.
73
+ //
74
+ // Endpoint-only scope is deliberate. `/v1/models` is public here, so the
75
+ // roster does not vary by key, and `charm-hyper` is absent from
76
+ // CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS — `ModelRegistry` resolves this
77
+ // namespace with no credential at all, so hashing one would split it
78
+ // against the namespace discovery computes and miss forever.
79
+ //
80
+ // Normalized through the shared helper because the registry passes the
81
+ // raw configured value while `charmHyperModelManagerOptions` passes a
82
+ // `/v1`-suffixed one; both must land on one namespace.
83
+ return `charm-hyper:models-v1:${Bun.hash(normalizeCharmHyperBaseUrl(options.baseUrl)).toString(36)}`;
84
+ }
65
85
  case "muse-code": {
66
86
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
67
87
  const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
@@ -69,10 +89,14 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
69
89
  }
70
90
  case "litellm": {
71
91
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
72
- // rich-v8 invalidates rows whose `compatConfig` retained a colliding
73
- // bundled model's provider-specific transport (e.g. Fireworks
74
- // `wireModelIdMode`) before that leak was fixed (issue #9938).
75
- return `litellm:rich-v8:${Bun.hash(baseUrl).toString(36)}`;
92
+ // rich-v9 unions compat across the management endpoints and keys the
93
+ // deployment's `supports_vision` declaration into it, so a warm
94
+ // rich-v8 row would keep retracting axes an earlier endpoint reported
95
+ // (issue #11982). rich-v8 invalidated rows whose `compatConfig`
96
+ // retained a colliding bundled model's provider-specific transport
97
+ // (e.g. Fireworks `wireModelIdMode`) before that leak was fixed
98
+ // (issue #9938).
99
+ return `litellm:rich-v9:${Bun.hash(baseUrl).toString(36)}`;
76
100
  }
77
101
  case "opencode-go":
78
102
  case "opencode-zen": {
@@ -37,6 +37,14 @@ export interface ProviderDescriptor {
37
37
  dynamicModelsAuthoritative?: boolean;
38
38
  /** Catalog discovery configuration. Only providers with this field participate in generate-models.ts. */
39
39
  catalogDiscovery?: CatalogDiscoveryConfig;
40
+ /**
41
+ * When true, generator backfills never copy reasoning/input/limits from
42
+ * same-id rows on other providers into this provider's rows. Set for
43
+ * providers whose endpoint discovery is the deployment truth and whose
44
+ * corrections live in KDL, so a stencil.so or canonical-family reference
45
+ * cannot reintroduce foreign metadata.
46
+ */
47
+ skipCrossProviderReferenceFills?: boolean;
40
48
  }
41
49
 
42
50
  /** A provider descriptor that has catalog discovery configured. */
@@ -77,6 +85,13 @@ export interface ProviderCatalogEntry {
77
85
  readonly dynamicModelsAuthoritative?: boolean;
78
86
  /** Catalog discovery configuration for generate-models.ts. */
79
87
  readonly catalogDiscovery?: CatalogDiscoveryConfig;
88
+ /**
89
+ * When true, generator backfills never copy reasoning/input/limits from
90
+ * same-id rows on other providers into this provider's rows. Set for
91
+ * providers whose endpoint discovery is the deployment truth and whose
92
+ * corrections live in KDL.
93
+ */
94
+ readonly skipCrossProviderReferenceFills?: boolean;
80
95
  /**
81
96
  * Built bespoke by the coding-agent runtime (OAuth-token-driven managers);
82
97
  * excluded from `PROVIDER_DESCRIPTORS` even though models are discoverable.
@@ -18,8 +18,10 @@ import {
18
18
  basetenModelManagerOptions,
19
19
  bedrockMantleModelManagerOptions,
20
20
  cerebrasModelManagerOptions,
21
+ charmHyperModelManagerOptions,
21
22
  clinePassModelManagerOptions,
22
23
  cloudflareAiGatewayModelManagerOptions,
24
+ commandCodeModelManagerOptions,
23
25
  coreWeaveModelManagerOptions,
24
26
  deepinfraModelManagerOptions,
25
27
  deepseekModelManagerOptions,
@@ -149,6 +151,26 @@ export const CATALOG_PROVIDERS = [
149
151
  createModelManagerOptions: (config: ModelManagerConfig) => cerebrasModelManagerOptions(config),
150
152
  catalogDiscovery: { label: "Cerebras" },
151
153
  },
154
+ {
155
+ id: "charm-hyper",
156
+ defaultModel: "glm-5.3",
157
+ envVars: ["CHARM_HYPER_API_KEY", "HYPER_API_KEY"],
158
+ createModelManagerOptions: (config: ModelManagerConfig) => charmHyperModelManagerOptions(config),
159
+ allowUnauthenticated: true,
160
+ dynamicModelsAuthoritative: true,
161
+ // The gateway row is the whole truth for a Hyper deployment. Same-id rows
162
+ // on other hosts disagree with it in both directions (it serves
163
+ // non-thinking Kimi K2.5/K2.7-Code and a text-only Gemma 4 that their
164
+ // upstream homes list as reasoning/vision), so foreign backfills would
165
+ // advertise capabilities this deployment does not have.
166
+ skipCrossProviderReferenceFills: true,
167
+ // Deliberately NO `catalogDiscovery`: that field is what enrolls a provider
168
+ // in generate-models.ts. This gateway's catalog is live deployment truth,
169
+ // so generating would freeze one hyper.charm.land snapshot into
170
+ // models.json — and since discovery here needs no credentials, it would
171
+ // happen on every regen, contradicting the runtime-only contract
172
+ // compat-conformance.test.ts pins for this provider.
173
+ },
152
174
  {
153
175
  id: "cloudflare-ai-gateway",
154
176
  defaultModel: "anthropic/claude-opus-4-8",
@@ -156,6 +178,19 @@ export const CATALOG_PROVIDERS = [
156
178
  createModelManagerOptions: (config: ModelManagerConfig) => cloudflareAiGatewayModelManagerOptions(config),
157
179
  catalogDiscovery: { label: "Cloudflare AI Gateway" },
158
180
  },
181
+ {
182
+ id: "commandcode",
183
+ defaultModel: "claude-sonnet-4-6",
184
+ envVars: ["COMMAND_CODE_API_KEY", "COMMANDCODE_API_KEY"],
185
+ createModelManagerOptions: (config: ModelManagerConfig) => commandCodeModelManagerOptions(config),
186
+ allowUnauthenticated: true,
187
+ dynamicModelsAuthoritative: true,
188
+ catalogDiscovery: { label: "Command Code", allowUnauthenticated: true },
189
+ // The Provider API rows carry no reasoning/modality metadata and KDL
190
+ // owns the deployment policy: same-id references on other hosts must
191
+ // not backfill reasoning, input, or limits during generation.
192
+ skipCrossProviderReferenceFills: true,
193
+ },
159
194
  {
160
195
  id: "cursor",
161
196
  defaultModel: "claude-4.6-opus-high",
@@ -611,6 +646,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = CATALOG_ENTRY
611
646
  createModelManagerOptions: provider.createModelManagerOptions,
612
647
  allowUnauthenticated: provider.allowUnauthenticated,
613
648
  dynamicModelsAuthoritative: provider.dynamicModelsAuthoritative,
649
+ skipCrossProviderReferenceFills: provider.skipCrossProviderReferenceFills,
614
650
  catalogDiscovery: provider.catalogDiscovery
615
651
  ? { ...provider.catalogDiscovery, envVars: provider.catalogDiscovery.envVars ?? provider.envVars ?? [] }
616
652
  : undefined,