@oh-my-pi/pi-catalog 18.4.4 → 18.4.6

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 (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/axes.d.ts +3 -1
  4. package/dist/types/compat/factory-droid.d.ts +65 -0
  5. package/dist/types/compat/provider-ids.d.ts +1 -1
  6. package/dist/types/compat/resolve.d.ts +8 -2
  7. package/dist/types/compat/types.d.ts +26 -1
  8. package/dist/types/discovery/cursor-proto.d.ts +284 -0
  9. package/dist/types/discovery/cursor.d.ts +12 -6
  10. package/dist/types/discovery/factory-droid.d.ts +28 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/discovery/protobuf.d.ts +3 -3
  13. package/dist/types/model-manager.d.ts +10 -0
  14. package/dist/types/models.d.ts +5 -1
  15. package/dist/types/provider-models/cache-provider-id.d.ts +2 -1
  16. package/dist/types/provider-models/descriptor-types.d.ts +2 -1
  17. package/dist/types/provider-models/openai-compat.d.ts +21 -0
  18. package/dist/types/provider-models/special.d.ts +2 -0
  19. package/dist/types/types.d.ts +100 -3
  20. package/dist/types/wire/cursor.d.ts +20 -0
  21. package/dist/types/wire/factory-droid.d.ts +45 -0
  22. package/package.json +4 -4
  23. package/src/build.ts +21 -0
  24. package/src/compat/auth-ids.ts +4 -0
  25. package/src/compat/axes.ts +113 -2
  26. package/src/compat/cascade.ts +8 -1
  27. package/src/compat/collapse.ts +3 -0
  28. package/src/compat/factory-droid.ts +170 -0
  29. package/src/compat/provider-ids.ts +2 -0
  30. package/src/compat/resolve.ts +47 -5
  31. package/src/compat/rules/README.md +18 -8
  32. package/src/compat/rules/auth/_order.kdl +2 -2
  33. package/src/compat/rules/auth/factory-droid.kdl +28 -0
  34. package/src/compat/rules/auth/helmcode.kdl +9 -0
  35. package/src/compat/rules/classes/anthropic.kdl +195 -80
  36. package/src/compat/rules/providers/commandcode.kdl +21 -5
  37. package/src/compat/rules/providers/cursor.kdl +29 -17
  38. package/src/compat/rules/providers/factory-droid.kdl +1166 -0
  39. package/src/compat/rules/providers/helmcode.kdl +120 -0
  40. package/src/compat/rules/runtime/behavior.kdl +65 -4
  41. package/src/compat/rules/taxonomy/_collapse.kdl +36 -14
  42. package/src/compat/rules.json +1 -1
  43. package/src/compat/types.ts +27 -1
  44. package/src/discovery/cursor-models.proto +261 -0
  45. package/src/discovery/cursor-proto.ts +471 -0
  46. package/src/discovery/cursor.ts +817 -88
  47. package/src/discovery/devin.ts +1 -3
  48. package/src/discovery/factory-droid.ts +296 -0
  49. package/src/discovery/index.ts +1 -0
  50. package/src/discovery/protobuf.ts +16 -7
  51. package/src/model-manager.ts +13 -0
  52. package/src/models.json +1 -1
  53. package/src/models.ts +13 -0
  54. package/src/provider-models/cache-provider-id.ts +37 -6
  55. package/src/provider-models/descriptor-types.ts +2 -1
  56. package/src/provider-models/descriptors.ts +4 -0
  57. package/src/provider-models/openai-compat.ts +83 -17
  58. package/src/provider-models/special.ts +26 -2
  59. package/src/types.ts +112 -1
  60. package/src/wire/cursor.ts +32 -0
  61. package/src/wire/factory-droid.ts +130 -0
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { Effort } from "../effort";
13
13
  import { MODEL_KINDS, type ThinkingControlMode } from "../types";
14
+ import { FACTORY_DROID_UPSTREAMS } from "../wire/factory-droid";
14
15
 
15
16
  /** Value shape a directive accepts (see `rules/README.md`). */
16
17
  export type AxisShape = "scalar" | "array" | "object";
@@ -19,7 +20,7 @@ export type AxisShape = "scalar" | "array" | "object";
19
20
  export type AxisSet = "wire" | "thinking" | "catalog";
20
21
 
21
22
  /** Resolved compat record families a wire axis may be assigned onto. */
22
- export type CompatRecordName = "openai" | "openai-responses" | "anthropic" | "bedrock" | "devin" | "google";
23
+ export type CompatRecordName = "openai" | "openai-responses" | "anthropic" | "bedrock" | "devin" | "google" | "request";
23
24
 
24
25
  /** One axis definition: resolved key, namespace, shape, and applicability. */
25
26
  export interface AxisDef {
@@ -37,6 +38,8 @@ export interface AxisDef {
37
38
  * that compile to camelCase resolved keys.
38
39
  */
39
40
  verbatimKeys?: true;
41
+ /** Array axes only: a bare directive assigns an empty list (an explicit "none"). */
42
+ emptyArray?: true;
40
43
  }
41
44
 
42
45
  const OAI = ["openai", "openai-responses"] as const;
@@ -86,6 +89,34 @@ function wire(
86
89
  * blocks.
87
90
  */
88
91
  export const AXES: Readonly<Record<string, AxisDef>> = {
92
+ // Selected-route request shaping, resolved alongside shared adapter compat.
93
+ "completions-reasoning-mode": wire("completionsReasoningMode", ["request"], "scalar", [
94
+ "none",
95
+ "effort",
96
+ "opt-in",
97
+ "forced-on",
98
+ ]),
99
+ "completions-reasoning-history": wire("completionsReasoningHistory", ["request"], "scalar", [
100
+ "omit",
101
+ "preserved",
102
+ "interleaved",
103
+ ]),
104
+ "anthropic-thinking": wire("anthropicThinking", ["request"], "scalar", [
105
+ "adaptive",
106
+ "adaptive-summarized",
107
+ "budget-interleaved",
108
+ "budget-effort",
109
+ ]),
110
+ "anthropic-tool-streaming-beta": wire("anthropicToolStreamingBeta", ["request"]),
111
+ "openai-platform-header": wire("openaiPlatformHeader", ["request"]),
112
+ "responses-cache-retention": wire("responsesCacheRetention", ["request"]),
113
+ "responses-verbosity": wire("responsesVerbosity", ["request"], "scalar", ["low"]),
114
+ "responses-service-tier": wire("responsesServiceTier", ["request"], "scalar", ["priority"]),
115
+ "responses-parallel-tool-calls": wire("responsesParallelToolCalls", ["request"]),
116
+ "responses-safety-identifier": wire("responsesSafetyIdentifier", ["request"]),
117
+ "responses-tool-choice-auto": wire("responsesToolChoiceAuto", ["request"]),
118
+ "routing-session-lock": wire("routingSessionLock", ["request"]),
119
+ "google-thinking": wire("googleThinking", ["request"], "scalar", ["level", "level-medium"]),
89
120
  // ── wire: OpenAI-compatible surfaces (chat completions + Responses) ──
90
121
  "allows-synthetic-reasoning-content-for-tool-calls": wire("allowsSyntheticReasoningContentForToolCalls", OAI),
91
122
  "always-send-max-tokens": wire("alwaysSendMaxTokens", OAI),
@@ -101,6 +132,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
101
132
  "include-encrypted-reasoning": wire("includeEncryptedReasoning", OAI),
102
133
  "kimi-api-format": wire("kimiApiFormat", ["openai"], "scalar", ["openai", "anthropic"]),
103
134
  "max-tokens-field": wire("maxTokensField", ["openai"], "scalar", ["max_completion_tokens", "max_tokens"]),
135
+ "mistral-reasoning-content-parts": wire("mistralReasoningContentParts", ["openai"]),
104
136
  "native-kimi-k3-reasoning": wire("nativeKimiK3Reasoning", ["openai"]),
105
137
  "omit-reasoning-effort": wire("omitReasoningEffort", OAI),
106
138
  "prompt-cache-breakpoint-ttl": wire("promptCacheBreakpointTtl", OAI, "scalar", ["30m"]),
@@ -128,6 +160,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
128
160
  ]),
129
161
  "reasoning-effort-map": wire("reasoningEffortMap", OAI, "object"),
130
162
  "replay-reasoning-content": wire("replayReasoningContent", ["openai"]),
163
+ "synthetic-reasoning-content-fallback": wire("syntheticReasoningContentFallback", ["openai"]),
131
164
  "requires-assistant-after-tool-result": wire("requiresAssistantAfterToolResult", ["openai"]),
132
165
  "requires-assistant-content-for-tool-calls": wire("requiresAssistantContentForToolCalls", OAI),
133
166
  "requires-mistral-tool-ids": wire("requiresMistralToolIds", ["openai"]),
@@ -194,7 +227,10 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
194
227
  "allow-anthropic-header-overrides": wire("allowAnthropicHeaderOverrides", ["anthropic"]),
195
228
  "disable-adaptive-thinking": wire("disableAdaptiveThinking", ["anthropic"]),
196
229
  "disable-strict-tools": wire("disableStrictTools", ["anthropic"]),
230
+ "disabled-thinking": wire("disabledThinking", ["anthropic"], "scalar", ["omit", "disabled", "adaptive"]),
231
+ "effort-beta": wire("effortBeta", ["anthropic"]),
197
232
  "escape-builtin-tool-names": wire("escapeBuiltinToolNames", ["anthropic"]),
233
+ "fast-mode": wire("fastMode", ["anthropic"]),
198
234
  "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
199
235
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
200
236
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
@@ -202,6 +238,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
202
238
  "requires-thinking-enabled": wire("requiresThinkingEnabled", ["anthropic"]),
203
239
  "requires-tool-result-id": wire("requiresToolResultId", ["anthropic"]),
204
240
  "signing-endpoint": wire("signingEndpoint", ["anthropic"]),
241
+ "strip-thinking-history": wire("stripThinkingHistory", ["anthropic"]),
205
242
  "supports-context-management": wire("supportsContextManagement", ["anthropic"]),
206
243
  "supports-output-effort": wire("supportsOutputEffort", ["anthropic"]),
207
244
  "supports-eager-tool-input-streaming": wire("supportsEagerToolInputStreaming", ["anthropic"]),
@@ -255,7 +292,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
255
292
  "stream-first-event-timeout-ms": wire("streamFirstEventTimeoutMs", [...OAI, "google"]),
256
293
  "stream-idle-timeout-ms": wire("streamIdleTimeoutMs", [...OAI, "anthropic", "bedrock", "google"]),
257
294
  "strip-image-input": wire("stripImageInput", [...OAI, "anthropic", "google"]),
258
- "supports-forced-tool-choice": wire("supportsForcedToolChoice", [...OAI, "anthropic"]),
295
+ "supports-forced-tool-choice": wire("supportsForcedToolChoice", [...OAI, "anthropic", "bedrock"]),
259
296
  "supports-sampling-params": wire("supportsSamplingParams", [...OAI, "anthropic"]),
260
297
  "thinking-loop-guard": wire("thinkingLoopGuard", [...OAI, "anthropic", "google"], "scalar", [
261
298
  "gemini",
@@ -321,6 +358,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
321
358
  "clamp-context-override": { key: "clampContextOverride", set: "catalog", shape: "scalar" },
322
359
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
323
360
  "context-window-floor": { key: "contextWindowFloor", set: "catalog", shape: "scalar" },
361
+ "context-window-authoritative": {
362
+ key: "contextWindowAuthoritative",
363
+ set: "catalog",
364
+ shape: "scalar",
365
+ values: [true, false],
366
+ },
324
367
  "cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
325
368
  "cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
326
369
  /**
@@ -353,6 +396,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
353
396
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
354
397
  "long-usage-limit-fallback": { key: "longUsageLimitFallback", set: "catalog", shape: "scalar" },
355
398
  "max-context-window": { key: "maxContextWindow", set: "catalog", shape: "scalar" },
399
+ "pricing-status": {
400
+ key: "pricingStatus",
401
+ set: "catalog",
402
+ shape: "scalar",
403
+ values: ["free", "included", "variable", "unknown"],
404
+ },
356
405
  "requires-cursor-tool-schema-projection": {
357
406
  key: "requiresCursorToolSchemaProjection",
358
407
  set: "catalog",
@@ -379,6 +428,68 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
379
428
  priority: { key: "priority", set: "catalog", shape: "scalar" },
380
429
  "service-tier-cost": { key: "serviceTierCost", set: "catalog", shape: "object" },
381
430
  "time-based-cost": { key: "timeBased", set: "catalog", shape: "object" },
431
+
432
+ // ── catalog: routed-subscription registry ──
433
+ // A gateway whose proxy fans one model out to several upstreams (Factory
434
+ // Droid). Consumed through `./factory-droid`; `api-routes` picks the wire
435
+ // and `quota-tiers` the billing pool.
436
+ /** Ordered upstream rotation; the first entry is the default `x-api-provider`. */
437
+ "upstream-rotation": { key: "upstreamRotation", set: "catalog", shape: "array", values: FACTORY_DROID_UPSTREAMS },
438
+ /**
439
+ * Upstreams eligible to serve one inference region. The provider-wide rule
440
+ * is the upstream serving table; a model rule replaces it (the native
441
+ * region override), and a bare directive means the region never serves it.
442
+ */
443
+ "region-upstreams-global": {
444
+ key: "regionUpstreamsGlobal",
445
+ set: "catalog",
446
+ shape: "array",
447
+ values: FACTORY_DROID_UPSTREAMS,
448
+ emptyArray: true,
449
+ },
450
+ "region-upstreams-us": {
451
+ key: "regionUpstreamsUs",
452
+ set: "catalog",
453
+ shape: "array",
454
+ values: FACTORY_DROID_UPSTREAMS,
455
+ emptyArray: true,
456
+ },
457
+ "region-upstreams-eu": {
458
+ key: "regionUpstreamsEu",
459
+ set: "catalog",
460
+ shape: "array",
461
+ values: FACTORY_DROID_UPSTREAMS,
462
+ emptyArray: true,
463
+ },
464
+ /** EU inference limits (`context-window`, `max-tokens`) where the region narrows the default. */
465
+ "region-limits-eu": { key: "regionLimitsEu", set: "catalog", shape: "object" },
466
+ /**
467
+ * Subscription credit rates: `input` is the per-token credit weight shown
468
+ * as the model's multiplier; `output` and `cache-read` multiply it.
469
+ */
470
+ "credit-rates": { key: "creditRates", set: "catalog", shape: "object" },
471
+ /**
472
+ * List price borrowed from a bundled catalog row: provider id, then the
473
+ * row id when it differs from the model's own. Absent means no list price.
474
+ */
475
+ "list-price-from": { key: "listPriceFrom", set: "catalog", shape: "array" },
476
+ /** Provider family whose live routing defaults apply to the model. */
477
+ "routing-family": {
478
+ key: "routingFamily",
479
+ set: "catalog",
480
+ shape: "scalar",
481
+ values: ["anthropic", "openai", "google", "factory", "xai"],
482
+ },
483
+ /** Other ids organization policy may use for the model. */
484
+ "policy-aliases": { key: "policyAliases", set: "catalog", shape: "array" },
485
+ /**
486
+ * Account gates: `feature-flag` must be on to list the model,
487
+ * `deprecation-flag` hides it once on, `requires-explicit-opt-in` hides it
488
+ * without an org policy, and `base-variant` marks a fast tier of that model.
489
+ */
490
+ entitlement: { key: "entitlement", set: "catalog", shape: "object" },
491
+ /** The native default when the caller picks no effort is thinking off. */
492
+ "default-reasoning-off": { key: "defaultReasoningOff", set: "catalog", shape: "scalar", values: [true, false] },
382
493
  };
383
494
 
384
495
  /** Records applicable to each API family; used by `resolve.ts` when applying wire axes. */
@@ -3,7 +3,7 @@
3
3
  * for one structured model target from the compiled rule tree.
4
4
  *
5
5
  * Faithful port of the o2 reference resolver (`cascade.rs`): rules are
6
- * conjunctions over `(class, provider, api, family, revision, models)`; per axis
6
+ * conjunctions over `(class, provider, api, upstream, family, revision, models)`; per axis
7
7
  * the matching rule with the greatest `(model-selector exactness,
8
8
  * constrained-dimension count, priority)` tuple wins, and an equal-tuple
9
9
  * same-axis contest throws {@link AmbiguousOverlapError}. Declaration and
@@ -129,6 +129,7 @@ function buildRuleIndex(cascade: CompiledCascade): RuleIndex {
129
129
  Number(compiled.class !== undefined) +
130
130
  Number(compiled.providers !== undefined) +
131
131
  Number(compiled.apis !== undefined) +
132
+ Number(compiled.upstreams !== undefined) +
132
133
  Number(compiled.family !== undefined) +
133
134
  Number(compiled.revision !== undefined) +
134
135
  Number(compiled.models !== undefined),
@@ -215,6 +216,11 @@ function rankRule(rule: IndexedRule, prepared: PreparedTarget): readonly [number
215
216
  const { compiled } = rule;
216
217
  const { target } = prepared;
217
218
  if (compiled.apis !== undefined && !compiled.apis.includes(target.api)) return undefined;
219
+ if (
220
+ compiled.upstreams !== undefined &&
221
+ (target.upstream === undefined || !compiled.upstreams.includes(target.upstream))
222
+ )
223
+ return undefined;
218
224
  if (compiled.family !== undefined && compiled.family !== target.family) return undefined;
219
225
  if (rule.revision !== undefined && (!prepared.revision || !revisionSatisfies(prepared.revision, rule.revision))) {
220
226
  return undefined;
@@ -292,6 +298,7 @@ function targetKey(target: ResolveTarget): string {
292
298
  return (
293
299
  keyPart(target.provider) +
294
300
  keyPart(target.api) +
301
+ keyPart(target.upstream) +
295
302
  keyPart(target.class) +
296
303
  keyPart(target.family) +
297
304
  keyPart(target.revision) +
@@ -937,6 +937,9 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
937
937
  ...(cursorMaxMode === undefined ? {} : { cursorMaxMode }),
938
938
  ...(cursorMaxModeRoutes === undefined ? {} : { cursorMaxModeRoutes }),
939
939
  };
940
+ if (memberSpecs.some(spec => spec.isProviderDefault === true)) {
941
+ collapsed.isProviderDefault = true;
942
+ }
940
943
  // The default wire id is the family's declared `defaultMember` when live,
941
944
  // else the highest-priority live member. Omitted when it equals the
942
945
  // logical id (bare/thinking pairs) — `resolveWireModelId` falls back.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Factory Droid registry policy read from KDL: the roster seed and per-model
3
+ * catalog axes in `rules/providers/factory-droid.kdl`, plus each model's wire
4
+ * (`api-routes`) in `rules/runtime/behavior.kdl`. Discovery builds
5
+ * account-scoped specs from it and the pi-ai provider re-reads it per request
6
+ * to bind a model to the account serving it.
7
+ */
8
+ import { isRecord, once } from "@oh-my-pi/pi-utils";
9
+ import type { Api, ModelSpec } from "../types";
10
+ import { FACTORY_DROID_WIRES, type FactoryDroidRegion, type FactoryDroidWire } from "../wire/factory-droid";
11
+ import { apiRouteFor } from "./behavior";
12
+ import { seedModels } from "./providers";
13
+ import { resolveCatalogAxes } from "./resolve";
14
+
15
+ const PROVIDER = "factory-droid";
16
+
17
+ /** Native token limits for one inference region. */
18
+ export interface FactoryDroidLimits {
19
+ contextWindow: number | null;
20
+ maxTokens: number | null;
21
+ }
22
+
23
+ /** Standard Credits rates: `input` is the per-token weight; `output` and `cacheRead` multiply it. */
24
+ export interface FactoryDroidCreditRates {
25
+ input: number;
26
+ output?: number;
27
+ cacheRead?: number;
28
+ }
29
+
30
+ /** Account gates on one registry model (the `entitlement` axis). */
31
+ export interface FactoryDroidEntitlement {
32
+ /** Feature flag that must be on for the account to see the model. */
33
+ featureFlag?: string;
34
+ /** Hard deprecation flag; when on, first-party clients hide the model. */
35
+ deprecationFlag?: string;
36
+ /** Hidden unless an org policy is known and does not require consent. */
37
+ requiresExplicitOptIn: boolean;
38
+ /** Base model this entry is the fast tier of; org policy can withdraw fast tiers as a class. */
39
+ baseVariant?: string;
40
+ }
41
+
42
+ /** Registry facts for one Factory model. */
43
+ export interface FactoryDroidModelPolicy {
44
+ wire: FactoryDroidWire;
45
+ /** Registry upstream order; the first entry is the default `x-api-provider`. */
46
+ rotation: readonly string[];
47
+ /** Upstreams eligible per inference region; empty means the region does not serve the model. */
48
+ regionUpstreams: Readonly<Record<FactoryDroidRegion, readonly string[]>>;
49
+ /** Registry limits; EU entries override them where the region narrows the model. */
50
+ limits: FactoryDroidLimits;
51
+ euLimits: Partial<FactoryDroidLimits>;
52
+ creditRates?: FactoryDroidCreditRates;
53
+ /** Bundled catalog row whose list price the model shows. */
54
+ listPriceFrom?: { provider: string; modelId: string };
55
+ /** Provider family whose live routing defaults apply. */
56
+ routingFamily?: string;
57
+ /** Other ids organization policy may use for the model. */
58
+ policyAliases: readonly string[];
59
+ entitlement: FactoryDroidEntitlement;
60
+ /** Native default with no caller effort is thinking off. */
61
+ defaultReasoningOff: boolean;
62
+ }
63
+
64
+ /** One registry model: its roster row and resolved policy. */
65
+ export interface FactoryDroidRegistryModel {
66
+ spec: ModelSpec<"factory-droid-agent">;
67
+ policy: FactoryDroidModelPolicy;
68
+ }
69
+
70
+ function stringList(value: unknown): readonly string[] | undefined {
71
+ return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : undefined;
72
+ }
73
+
74
+ function numberField(source: Record<string, unknown>, key: string): number | undefined {
75
+ const value = source[key];
76
+ return typeof value === "number" ? value : undefined;
77
+ }
78
+
79
+ function stringField(source: Record<string, unknown>, key: string): string | undefined {
80
+ const value = source[key];
81
+ return typeof value === "string" ? value : undefined;
82
+ }
83
+
84
+ function readPolicy(spec: ModelSpec<"factory-droid-agent">): FactoryDroidModelPolicy {
85
+ const catalog = resolveCatalogAxes(spec);
86
+ const routedApi = apiRouteFor(PROVIDER, spec.id)?.api;
87
+ const euLimits = isRecord(catalog.regionLimitsEu) ? catalog.regionLimitsEu : {};
88
+ const credits = isRecord(catalog.creditRates) ? catalog.creditRates : {};
89
+ const entitlement = isRecord(catalog.entitlement) ? catalog.entitlement : {};
90
+ const creditInput = numberField(credits, "input");
91
+ const [priceProvider, priceModelId] = stringList(catalog.listPriceFrom) ?? [];
92
+ const euContextWindow = numberField(euLimits, "contextWindow");
93
+ const euMaxTokens = numberField(euLimits, "maxTokens");
94
+ const creditOutput = numberField(credits, "output");
95
+ const creditCacheRead = numberField(credits, "cacheRead");
96
+ const featureFlag = stringField(entitlement, "featureFlag");
97
+ const deprecationFlag = stringField(entitlement, "deprecationFlag");
98
+ const baseVariant = stringField(entitlement, "baseVariant");
99
+ return {
100
+ wire: FACTORY_DROID_WIRES.find(wire => wire === routedApi) ?? "openai-completions",
101
+ rotation: stringList(catalog.upstreamRotation) ?? [],
102
+ regionUpstreams: {
103
+ global: stringList(catalog.regionUpstreamsGlobal) ?? [],
104
+ us: stringList(catalog.regionUpstreamsUs) ?? [],
105
+ eu: stringList(catalog.regionUpstreamsEu) ?? [],
106
+ },
107
+ limits: { contextWindow: spec.contextWindow, maxTokens: spec.maxTokens },
108
+ euLimits: {
109
+ ...(euContextWindow !== undefined && { contextWindow: euContextWindow }),
110
+ ...(euMaxTokens !== undefined && { maxTokens: euMaxTokens }),
111
+ },
112
+ ...(creditInput !== undefined && {
113
+ creditRates: {
114
+ input: creditInput,
115
+ ...(creditOutput !== undefined && { output: creditOutput }),
116
+ ...(creditCacheRead !== undefined && { cacheRead: creditCacheRead }),
117
+ },
118
+ }),
119
+ ...(priceProvider !== undefined && {
120
+ listPriceFrom: { provider: priceProvider, modelId: priceModelId ?? spec.id },
121
+ }),
122
+ ...(typeof catalog.routingFamily === "string" && { routingFamily: catalog.routingFamily }),
123
+ policyAliases: stringList(catalog.policyAliases) ?? [],
124
+ entitlement: {
125
+ ...(featureFlag !== undefined && { featureFlag }),
126
+ ...(deprecationFlag !== undefined && { deprecationFlag }),
127
+ requiresExplicitOptIn: entitlement.requiresExplicitOptIn === true,
128
+ ...(baseVariant !== undefined && { baseVariant }),
129
+ },
130
+ defaultReasoningOff: catalog.defaultReasoningOff === true,
131
+ };
132
+ }
133
+
134
+ /** The registry roster in declaration order, each row with its resolved policy. */
135
+ export const factoryDroidRegistry = once((): readonly FactoryDroidRegistryModel[] =>
136
+ seedModels<"factory-droid-agent">(PROVIDER).map(spec => ({ spec, policy: readPolicy(spec) })),
137
+ );
138
+
139
+ /** Registry policy for a model (by its request id); undefined for ids outside the registry. */
140
+ export function resolveFactoryDroidPolicy(
141
+ model: Pick<ModelSpec<Api>, "id" | "requestModelId">,
142
+ ): FactoryDroidModelPolicy | undefined {
143
+ const id = model.requestModelId ?? model.id;
144
+ return factoryDroidRegistry().find(entry => entry.spec.id === id)?.policy;
145
+ }
146
+
147
+ /**
148
+ * Effective upstream rotation for an inference region: the registry order,
149
+ * narrowed to the upstreams that region serves (or the model's own region
150
+ * override). An empty result means the region cannot serve the model.
151
+ */
152
+ export function resolveFactoryDroidRotation(
153
+ policy: FactoryDroidModelPolicy,
154
+ region: string | undefined,
155
+ ): readonly string[] {
156
+ const eligible = policy.regionUpstreams[region === "eu" || region === "us" ? region : "global"];
157
+ return policy.rotation.filter(upstream => eligible.includes(upstream));
158
+ }
159
+
160
+ /** Native limits for an inference region; EU overrides fall back to the registry defaults. */
161
+ export function factoryDroidRegionalLimits(
162
+ policy: FactoryDroidModelPolicy,
163
+ region: string | undefined,
164
+ ): FactoryDroidLimits {
165
+ if (region !== "eu") return policy.limits;
166
+ return {
167
+ contextWindow: policy.euLimits.contextWindow ?? policy.limits.contextWindow,
168
+ maxTokens: policy.euLimits.maxTokens ?? policy.limits.maxTokens,
169
+ };
170
+ }
@@ -23,6 +23,7 @@ export type KnownProvider =
23
23
  | "deepinfra"
24
24
  | "deepseek"
25
25
  | "devin"
26
+ | "factory-droid"
26
27
  | "firepass"
27
28
  | "fireworks"
28
29
  | "github-copilot"
@@ -34,6 +35,7 @@ export type KnownProvider =
34
35
  | "google-gemini-cli"
35
36
  | "google-vertex"
36
37
  | "groq"
38
+ | "helmcode"
37
39
  | "huggingface"
38
40
  | "kilo"
39
41
  | "kimi-code"
@@ -42,7 +42,7 @@ import { API_COMPAT_RECORDS, AXES, type CompatRecordName } from "./axes";
42
42
  import { hasModelScopedEffortsRule, resolveCascade } from "./cascade";
43
43
  import { compareRevision, parseRevision, type Revision } from "./revision";
44
44
  import { classifyModel, stripThinkingVariantSuffix } from "./taxonomy";
45
- import type { ModelIdentity, ResolvedAxes, ResolveTarget } from "./types";
45
+ import type { ModelIdentity, RequestPolicy, ResolvedAxes, ResolveTarget } from "./types";
46
46
 
47
47
  /** Result of resolving one model spec through the compat engine. */
48
48
  export interface ResolvedModelPolicy<TApi extends Api = Api> {
@@ -51,6 +51,25 @@ export interface ResolvedModelPolicy<TApi extends Api = Api> {
51
51
  thinking: ThinkingConfig | undefined;
52
52
  /** Catalog-data axis assignments (longContext, priority, …) for generation. */
53
53
  catalog: Record<string, unknown>;
54
+ /** Request-shaping directives for the selected upstream. */
55
+ request: RequestPolicy;
56
+ }
57
+
58
+ /** Request routing context is separate from model identity and deployment. */
59
+ export interface ResolveRoute {
60
+ upstream?: string;
61
+ }
62
+
63
+ const REQUEST_AXIS_KEYS = Object.values(AXES)
64
+ .filter(axis => axis.records?.includes("request"))
65
+ .map(axis => axis.key);
66
+
67
+ function resolveRequestPolicy(axes: ResolvedAxes): RequestPolicy {
68
+ const policy: Record<string, unknown> = {};
69
+ for (const key of REQUEST_AXIS_KEYS) {
70
+ if (key in axes.wire) policy[key] = axes.wire[key];
71
+ }
72
+ return policy as RequestPolicy;
54
73
  }
55
74
 
56
75
  // ---------------------------------------------------------------------------
@@ -516,6 +535,7 @@ function detectOpenAICompat(
516
535
  filterReasoningHistory: d.isOpenRouter && isAnthropicModel,
517
536
  thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
518
537
  reasoningContentField: d.isClinePass ? "reasoning" : "reasoning_content",
538
+ mistralReasoningContentParts: undefined,
519
539
  requiresReasoningContentForToolCalls:
520
540
  (facts.is("kimi") && !d.isOpenCodeProvider) ||
521
541
  (isDeepseekFamily && reasoningCapable) ||
@@ -524,6 +544,8 @@ function detectOpenAICompat(
524
544
  requiresReasoningContentForAllAssistantTurns:
525
545
  ((isDeepseekFamily && reasoningCapable) || d.isXiaomiMimo) && !d.isOpenRouter,
526
546
  allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !reasoningCapable) && !d.isXiaomiMimo,
547
+ // Keep the typed sparse override key present for DeepSeek proxy replay.
548
+ syntheticReasoningContentFallback: undefined,
527
549
  replayReasoningContent: d.isLocalOpenAICompatBackend,
528
550
  qwenPreserveThinking:
529
551
  (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && d.isLocalOpenAICompatBackend,
@@ -756,6 +778,7 @@ function resolveOpenAIResponsesPolicy(
756
778
  reasoningCapable,
757
779
  requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,
758
780
  allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,
781
+ syntheticReasoningContentFallback: undefined,
759
782
  replayReasoningContent: false,
760
783
  qwenPreserveThinking: false,
761
784
  qwenTemplateReasoningEffort: false,
@@ -892,6 +915,9 @@ function resolveAnthropicPolicy(
892
915
  injectClaudeCodeInstruction: true,
893
916
  stripImageInput: false,
894
917
  thinkingLoopGuard: undefined,
918
+ // Present as keys so models.yml `compat` can set them; unset unless a rule assigns them.
919
+ stripThinkingHistory: undefined,
920
+ fastMode: undefined,
895
921
  streamIdleTimeoutMs: spec.compat?.streamIdleTimeoutMs,
896
922
  };
897
923
  applyWireAxes(compat, axes.wire, "anthropic-messages");
@@ -909,6 +935,7 @@ function resolveBedrockPolicy(spec: ModelSpec<"bedrock-converse-stream">, axes:
909
935
  supportsLongPromptCacheRetention: false,
910
936
  promptCacheMinimumTokens: 0,
911
937
  promptCacheMaximumCheckpoints: 0,
938
+ supportsForcedToolChoice: true,
912
939
  };
913
940
  // Reasoning capability is a mechanism gate; adaptive-lineage duration is rule-owned.
914
941
  compat.streamIdleTimeoutMs = compatReasoning(spec, axes) ? BEDROCK_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined;
@@ -1207,7 +1234,15 @@ function fillExplicitThinking<TApi extends Api>(
1207
1234
  (impliesMandatoryReasoning(facts, spec.id) || isQwenTemplateReasoningEffortCompat(compat)));
1208
1235
  const needsDefaultLevel = thinking.defaultLevel === undefined && rule.defaultLevel !== undefined;
1209
1236
  const needsPrefixBinding = thinking.prefixBinding === undefined && rule.prefixBinding === true;
1210
- if (effortMap === undefined && !needsDisplay && !needsRequiresEffort && !needsDefaultLevel && !needsPrefixBinding) {
1237
+ const needsEffortBudgets = thinking.effortBudgets === undefined && rule.effortBudgets !== undefined;
1238
+ if (
1239
+ effortMap === undefined &&
1240
+ !needsDisplay &&
1241
+ !needsRequiresEffort &&
1242
+ !needsDefaultLevel &&
1243
+ !needsPrefixBinding &&
1244
+ !needsEffortBudgets
1245
+ ) {
1211
1246
  return thinking;
1212
1247
  }
1213
1248
  const filled: ThinkingConfig = { ...thinking };
@@ -1216,6 +1251,7 @@ function fillExplicitThinking<TApi extends Api>(
1216
1251
  if (needsDefaultLevel && rule.defaultLevel !== undefined) filled.defaultLevel = rule.defaultLevel;
1217
1252
  if (needsRequiresEffort) filled.requiresEffort = true;
1218
1253
  if (needsPrefixBinding) filled.prefixBinding = true;
1254
+ if (needsEffortBudgets) filled.effortBudgets = rule.effortBudgets;
1219
1255
  return filled;
1220
1256
  }
1221
1257
 
@@ -1264,11 +1300,16 @@ export function resolveCatalogAxes(spec: ModelSpec<Api>): Record<string, unknown
1264
1300
  * Resolves the full policy surface for one model spec: structured identity,
1265
1301
  * complete compat record, thinking metadata, and catalog-data corrections.
1266
1302
  */
1267
- export function resolveModelPolicy<TApi extends Api>(spec: ModelSpec<TApi>): ResolvedModelPolicy<TApi>;
1268
- export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Api> {
1303
+ export function resolveModelPolicy<TApi extends Api>(
1304
+ spec: ModelSpec<TApi>,
1305
+ route?: ResolveRoute,
1306
+ ): ResolvedModelPolicy<TApi>;
1307
+ export function resolveModelPolicy(spec: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1269
1308
  const identity = resolveIdentity(spec);
1270
1309
  const facts = new IdentityFacts(identity);
1271
- const axes = resolveCascade(buildResolveTarget(spec, identity));
1310
+ const target = buildResolveTarget(spec, identity);
1311
+ target.upstream = route?.upstream;
1312
+ const axes = resolveCascade(target);
1272
1313
  let compat: CompatOf<Api>;
1273
1314
  if (specUsesApi(spec, "openrouter")) {
1274
1315
  const chat = resolveOpenAICompletionsPolicy(spec, facts, axes);
@@ -1302,6 +1343,7 @@ export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Ap
1302
1343
  compat,
1303
1344
  thinking: resolveThinkingPolicy(spec, facts, axes, compat),
1304
1345
  catalog: axes.catalog,
1346
+ request: resolveRequestPolicy(axes),
1305
1347
  };
1306
1348
  }
1307
1349
 
@@ -152,7 +152,7 @@ discovery {
152
152
 
153
153
  ## Cascade grammar
154
154
 
155
- A cascade document starts with `class`, `provider`, or `on-api`. Root `on-api` declares a transport contract independent of model identity and provider name. Every selector adds a conjunct to the current rule. `on` scopes by deployment provider; `on-api` scopes by request adapter, including custom provider names. Axis directives may appear directly in any permitted scope, and nested selector blocks may appear alongside them.
155
+ A cascade document starts with `class`, `provider`, or `on-api`. Root `on-api` declares a transport contract independent of model identity and provider name. Every selector adds a conjunct to the current rule. `on` scopes by deployment provider; `on-api` scopes by request adapter, including custom provider names; `on-upstream` scopes by the actual upstream selected behind that deployment. An upstream never changes the deployment provider or imports that upstream's direct-host rules. Axis directives may appear directly in any permitted scope, and nested selector blocks may appear alongside them.
156
156
 
157
157
  ```kdl
158
158
  class "gemini" {
@@ -182,14 +182,17 @@ provider "openrouter" {
182
182
  | Selector | Form | Matching semantics |
183
183
  | ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
184
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. At document root it may contain `class` and `models`; under a root `class` it may contain `family`, `revision`, and `models`. |
185
+ | `provider` | `provider "id" { ... }` | Exact provider ID. It is root-only and may contain `class`, `on-api`, and `models`. |
186
+ | `on` | `on "provider-a" "provider-b" { ... }` | One or more provider IDs, combined as OR. It is allowed under a root `class`, and may contain `on-api` to conjoin an API scope plus `family`, `revision`, and `models`. |
187
+ | `on-api` | `on-api "adapter-a" "adapter-b" { ... }` | One or more request adapter IDs, combined as OR. At document root it may contain `class` and `models`; under a root `class` (directly or nested inside `on`) or `provider` it may contain `family`, `revision`, and `models`. |
188
188
  | `family` | `family "id" { ... }` | Exact classified family ID. It may contain `revision` and `models`. A target with no family does not match. |
189
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
+ | `models` | `models "id" "vendor/*" { ... }` | One or more alternatives, combined as OR. It may contain only `on-upstream`. `token="name"` matches an ASCII-case-insensitive token bounded by non-alphanumerics. |
191
+ | `on-upstream` | `on-upstream "a" "b" { ... }` | Exact selected upstream IDs, combined as OR. Allowed inside any selector scope once; preserves the containing scope's other permitted children. Absent upstream never matches. |
191
192
 
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.
193
+ Every selector scope may additionally contain `on-upstream`; it cannot replace an already constrained upstream. Class, provider/`on`, `on-api`, `on-upstream`, and family 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.
194
+
195
+ An `on-api` nested inside `on` under a class requires both the selected provider and request adapter; values within each selector remain alternatives.
193
196
 
194
197
  A `models` string without `*` is an exact, case-sensitive match against the provider-relative model identifier. A string containing `*` is an anchored, ASCII-case-insensitive wildcard match. Prefer taxonomy ranks; retain exact/glob lists only when they isolate the census member set exactly, and keep a `// residue:` comment explaining why ranks do not.
195
198
 
@@ -197,6 +200,8 @@ A `models` string without `*` is an exact, case-sensitive match against the prov
197
200
 
198
201
  `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
202
 
203
+ `resolveModelPolicy(spec, { upstream })` resolves the selected route without mutating the model identity. Call it again when retry routing selects another upstream. Its typed `request` record carries request-shaping axes (reasoning history, disabled-effort semantics, Anthropic thinking mode, Responses options, and Google thinking dialect); shared transport fields remain in `compat`. Both records use the same per-axis cascade, precedence, ambiguity checks, and upstream-aware cache key. Request fields have the `request` applicability record in `axes.ts`, so they do not leak into shared transport compat objects. See [`providers/factory-droid.kdl`](providers/factory-droid.kdl) for the upstream-scoped consumer.
204
+
200
205
  ### Axis vocabulary and value shapes
201
206
 
202
207
  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.
@@ -204,12 +209,16 @@ The directive vocabulary is closed and lives in **`src/compat/axes.ts`** — one
204
209
  The three value shapes are:
205
210
 
206
211
  - **Scalar**: exactly one KDL boolean, integer, float, or string argument and no children. `#null` is rejected.
207
- - **Array**: one or more scalar arguments and no children; it resolves to a JSON array.
212
+ - **Array**: one or more scalar arguments and no children; it resolves to a JSON array. Axes marked `emptyArray` in `axes.ts` also accept a bare directive, which assigns an explicit empty list (`region-upstreams-eu` with no arguments: the region never serves the model).
208
213
  - **Object**: no arguments and a child block, including an empty block. Child names are kebab-case: an axis-directive spelling compiles to its resolved axis key (`template-reasoning-effort` → `qwenTemplateReasoningEffort`), anything else converts mechanically (`input-threshold` → `inputThreshold`); camelCase names are a compile error. `extra-body` payloads (top-level or nested) are the exception — their child names are literal wire JSON keys copied verbatim (`enable_thinking`). Each child is either one scalar or another object; arrays are not representable inside an object payload.
209
214
 
210
215
  A rule cannot assign the same resolved axis twice in one block.
211
216
  One object axis carries a computed form: `long-context-cost` accepts either the absolute rates (`input-threshold` + `input`/`output`/`cache-read`/`cache-write`) or `input-threshold` + `multiplier` (with optional `input-threshold-inclusive`), which derives the tier from the row's live base price at build time so the rule tracks upstream list-price updates (xAI's SuperGrok 200K tier). Rows without a token price carry no tier.
212
217
 
218
+ `context-window-authoritative #true` preserves a host's supplied context window through runtime model selection instead of applying inferred expansion or reference-price-tier caps. Explicit user context overrides still apply afterward. It applies to rows materialized through `buildModel` (discovery and regenerated bundles). See [`providers/factory-droid.kdl`](providers/factory-droid.kdl).
219
+
220
+ The routed-subscription registry axes (`upstream-rotation`, `region-upstreams-global|us|eu`, `region-limits-eu`, `credit-rates`, `list-price-from`, `routing-family`, `policy-aliases`, `entitlement`, `default-reasoning-off`) describe a gateway whose proxy fans one model out to several upstreams. They are read through `src/compat/factory-droid.ts` by Factory Droid discovery and its request provider, not materialized by `buildModel`. A provider-wide `region-upstreams-*` rule is the upstream serving table and a model rule replaces it for that region. `list-price-from "<provider>" ["<id>"]` shows a bundled row's list price beside the subscription's own billing; unlike seed values it is resolved at runtime and degrades to the seed's zero cost when the row is gone. See [`providers/factory-droid.kdl`](providers/factory-droid.kdl), whose wire and billing pool per model are `api-routes` and `quota-tiers` rules in `runtime/behavior.kdl`.
221
+
213
222
  ### Time-based pricing
214
223
 
215
224
  The catalog object axis `time-based-cost` materializes into `ModelCost.timeBased`. It uses **named child objects**, not KDL arrays or repeated anonymous windows. For example, inside a matching provider/model scope:
@@ -258,7 +267,7 @@ Rules resolve independently per axis. A matching rule is ranked by:
258
267
  The tuple is compared lexicographically, greatest first:
259
268
 
260
269
  - model exactness is `2` when any matching `models` selector is exact, `1` when the best matching selector is a glob or token, and `0` when the rule has no `models` selector;
261
- - dimension count is the number of present dimensions among class, provider/`on`, API/`on-api`, family, revision, and models;
270
+ - dimension count is the number of present dimensions among class, provider/`on`, API/`on-api`, upstream/`on-upstream`, family, revision, and models;
262
271
  - priority is the local block's `priority`, defaulting to `0`.
263
272
 
264
273
  The highest-ranked matching assignment wins for that axis. Two distinct rules that tie on all three components and assign the same axis are an ambiguity error even if their values are equal. File and declaration order never resolve the tie; add an explicit priority only after confirming the overlap is intentional.
@@ -417,6 +426,7 @@ A `seed` _defines_ bundled rows for providers whose catalog cannot be discovered
417
426
  | `always` | Every regeneration. Same-id upstream/discovery rows win dedup. |
418
427
  | `fallback` | Only when the provider's authoritative catalog discovery did not succeed. |
419
428
  | `empty` | Only when no other source produced a row for the provider. |
429
+ | `never` | Never; the provider's runtime model manager is the only consumer. |
420
430
 
421
431
  `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.
422
432