@oh-my-pi/pi-catalog 18.4.3 → 18.4.5

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 (68) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/types/compat/anthropic.d.ts +6 -0
  3. package/dist/types/compat/auth-ids.d.ts +2 -2
  4. package/dist/types/compat/axes.d.ts +3 -1
  5. package/dist/types/compat/factory-droid.d.ts +65 -0
  6. package/dist/types/compat/provider-ids.d.ts +1 -1
  7. package/dist/types/compat/resolve.d.ts +8 -2
  8. package/dist/types/compat/types.d.ts +26 -1
  9. package/dist/types/discovery/cursor-proto.d.ts +284 -0
  10. package/dist/types/discovery/cursor.d.ts +12 -6
  11. package/dist/types/discovery/factory-droid.d.ts +28 -0
  12. package/dist/types/discovery/index.d.ts +1 -0
  13. package/dist/types/discovery/protobuf.d.ts +3 -3
  14. package/dist/types/hosts.d.ts +13 -0
  15. package/dist/types/model-manager.d.ts +10 -0
  16. package/dist/types/models.d.ts +5 -1
  17. package/dist/types/provider-models/cache-provider-id.d.ts +2 -1
  18. package/dist/types/provider-models/descriptor-types.d.ts +2 -1
  19. package/dist/types/provider-models/openai-compat.d.ts +21 -0
  20. package/dist/types/provider-models/special.d.ts +2 -0
  21. package/dist/types/types.d.ts +115 -3
  22. package/dist/types/wire/codex.d.ts +5 -3
  23. package/dist/types/wire/cursor.d.ts +20 -0
  24. package/dist/types/wire/factory-droid.d.ts +45 -0
  25. package/package.json +4 -4
  26. package/src/build.ts +19 -0
  27. package/src/compat/anthropic.ts +11 -1
  28. package/src/compat/auth-ids.ts +4 -0
  29. package/src/compat/axes.ts +114 -1
  30. package/src/compat/cascade.ts +8 -1
  31. package/src/compat/collapse.ts +3 -0
  32. package/src/compat/factory-droid.ts +170 -0
  33. package/src/compat/provider-ids.ts +2 -0
  34. package/src/compat/resolve.ts +59 -8
  35. package/src/compat/rules/README.md +15 -7
  36. package/src/compat/rules/auth/_order.kdl +2 -2
  37. package/src/compat/rules/auth/factory-droid.kdl +28 -0
  38. package/src/compat/rules/auth/helmcode.kdl +9 -0
  39. package/src/compat/rules/classes/anthropic.kdl +16 -4
  40. package/src/compat/rules/providers/cursor.kdl +29 -17
  41. package/src/compat/rules/providers/factory-droid.kdl +1166 -0
  42. package/src/compat/rules/providers/helmcode.kdl +120 -0
  43. package/src/compat/rules/providers/openai-codex.kdl +17 -0
  44. package/src/compat/rules/runtime/behavior.kdl +44 -0
  45. package/src/compat/rules/taxonomy/_collapse.kdl +36 -14
  46. package/src/compat/rules.json +1 -1
  47. package/src/compat/types.ts +27 -1
  48. package/src/discovery/codex.ts +16 -0
  49. package/src/discovery/cursor-models.proto +261 -0
  50. package/src/discovery/cursor-proto.ts +471 -0
  51. package/src/discovery/cursor.ts +817 -88
  52. package/src/discovery/devin.ts +1 -3
  53. package/src/discovery/factory-droid.ts +296 -0
  54. package/src/discovery/index.ts +1 -0
  55. package/src/discovery/protobuf.ts +16 -7
  56. package/src/hosts.ts +47 -0
  57. package/src/model-manager.ts +13 -0
  58. package/src/models.json +1 -1
  59. package/src/models.ts +13 -0
  60. package/src/provider-models/cache-provider-id.ts +37 -6
  61. package/src/provider-models/descriptor-types.ts +2 -1
  62. package/src/provider-models/descriptors.ts +4 -0
  63. package/src/provider-models/openai-compat.ts +83 -17
  64. package/src/provider-models/special.ts +26 -2
  65. package/src/types.ts +129 -1
  66. package/src/wire/codex.ts +5 -3
  67. package/src/wire/cursor.ts +32 -0
  68. 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"]),
@@ -190,10 +223,14 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
190
223
  "zai-reasoning-effort-dialect": wire("zaiReasoningEffortDialect", ["openai"]),
191
224
 
192
225
  // ── wire: anthropic-messages ──
226
+ "bedrock-messages-api": wire("bedrockMessagesApi", ["anthropic"]),
193
227
  "allow-anthropic-header-overrides": wire("allowAnthropicHeaderOverrides", ["anthropic"]),
194
228
  "disable-adaptive-thinking": wire("disableAdaptiveThinking", ["anthropic"]),
195
229
  "disable-strict-tools": wire("disableStrictTools", ["anthropic"]),
230
+ "disabled-thinking": wire("disabledThinking", ["anthropic"], "scalar", ["omit", "disabled", "adaptive"]),
231
+ "effort-beta": wire("effortBeta", ["anthropic"]),
196
232
  "escape-builtin-tool-names": wire("escapeBuiltinToolNames", ["anthropic"]),
233
+ "fast-mode": wire("fastMode", ["anthropic"]),
197
234
  "first-party-provider": wire("firstPartyProvider", ["anthropic"]),
198
235
  "inject-claude-code-instruction": wire("injectClaudeCodeInstruction", ["anthropic"]),
199
236
  "official-endpoint": wire("officialEndpoint", ["anthropic", "openai-responses"]),
@@ -201,6 +238,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
201
238
  "requires-thinking-enabled": wire("requiresThinkingEnabled", ["anthropic"]),
202
239
  "requires-tool-result-id": wire("requiresToolResultId", ["anthropic"]),
203
240
  "signing-endpoint": wire("signingEndpoint", ["anthropic"]),
241
+ "strip-thinking-history": wire("stripThinkingHistory", ["anthropic"]),
204
242
  "supports-context-management": wire("supportsContextManagement", ["anthropic"]),
205
243
  "supports-output-effort": wire("supportsOutputEffort", ["anthropic"]),
206
244
  "supports-eager-tool-input-streaming": wire("supportsEagerToolInputStreaming", ["anthropic"]),
@@ -209,6 +247,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
209
247
  "supports-mid-conversation-tool-changes": wire("supportsMidConversationToolChanges", ["anthropic"]),
210
248
  "supports-per-message-effort": wire("supportsPerMessageEffort", ["anthropic"]),
211
249
  "supports-server-compaction": wire("supportsServerCompaction", ["anthropic"]),
250
+ "supports-between-tools-thinking": wire("supportsBetweenToolsThinking", ["anthropic"]),
212
251
  "supports-thinking-binding-controls": wire("supportsThinkingBindingControls", ["anthropic"]),
213
252
  "supports-turn-scoped-system": wire("supportsTurnScopedSystem", ["anthropic"]),
214
253
 
@@ -319,6 +358,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
319
358
  "clamp-context-override": { key: "clampContextOverride", set: "catalog", shape: "scalar" },
320
359
  "context-promotion-target": { key: "contextPromotionTarget", set: "catalog", shape: "scalar" },
321
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
+ },
322
367
  "cost-patch": { key: "costPatch", set: "catalog", shape: "object" },
323
368
  "cost-fallback": { key: "costFallback", set: "catalog", shape: "object" },
324
369
  /**
@@ -351,6 +396,12 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
351
396
  "prompt-cache": { key: "promptCache", set: "catalog", shape: "object" },
352
397
  "long-usage-limit-fallback": { key: "longUsageLimitFallback", set: "catalog", shape: "scalar" },
353
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
+ },
354
405
  "requires-cursor-tool-schema-projection": {
355
406
  key: "requiresCursorToolSchemaProjection",
356
407
  set: "catalog",
@@ -377,6 +428,68 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
377
428
  priority: { key: "priority", set: "catalog", shape: "scalar" },
378
429
  "service-tier-cost": { key: "serviceTierCost", set: "catalog", shape: "object" },
379
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] },
380
493
  };
381
494
 
382
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"
@@ -31,13 +31,18 @@ import type {
31
31
  ResolvedOpenAISharedCompat,
32
32
  ThinkingConfig,
33
33
  } from "../types";
34
- import { isAnthropicSigningProxyUrl, isAzureAnthropicRoute, isOfficialAnthropicApiUrl } from "./anthropic";
34
+ import {
35
+ isAnthropicSigningProxyUrl,
36
+ isAzureAnthropicRoute,
37
+ isBedrockAnthropicRoute,
38
+ isOfficialAnthropicApiUrl,
39
+ } from "./anthropic";
35
40
  import { applyCompatOverrides } from "./apply";
36
41
  import { API_COMPAT_RECORDS, AXES, type CompatRecordName } from "./axes";
37
42
  import { hasModelScopedEffortsRule, resolveCascade } from "./cascade";
38
43
  import { compareRevision, parseRevision, type Revision } from "./revision";
39
44
  import { classifyModel, stripThinkingVariantSuffix } from "./taxonomy";
40
- import type { ModelIdentity, ResolvedAxes, ResolveTarget } from "./types";
45
+ import type { ModelIdentity, RequestPolicy, ResolvedAxes, ResolveTarget } from "./types";
41
46
 
42
47
  /** Result of resolving one model spec through the compat engine. */
43
48
  export interface ResolvedModelPolicy<TApi extends Api = Api> {
@@ -46,6 +51,25 @@ export interface ResolvedModelPolicy<TApi extends Api = Api> {
46
51
  thinking: ThinkingConfig | undefined;
47
52
  /** Catalog-data axis assignments (longContext, priority, …) for generation. */
48
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;
49
73
  }
50
74
 
51
75
  // ---------------------------------------------------------------------------
@@ -511,6 +535,7 @@ function detectOpenAICompat(
511
535
  filterReasoningHistory: d.isOpenRouter && isAnthropicModel,
512
536
  thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
513
537
  reasoningContentField: d.isClinePass ? "reasoning" : "reasoning_content",
538
+ mistralReasoningContentParts: undefined,
514
539
  requiresReasoningContentForToolCalls:
515
540
  (facts.is("kimi") && !d.isOpenCodeProvider) ||
516
541
  (isDeepseekFamily && reasoningCapable) ||
@@ -519,6 +544,8 @@ function detectOpenAICompat(
519
544
  requiresReasoningContentForAllAssistantTurns:
520
545
  ((isDeepseekFamily && reasoningCapable) || d.isXiaomiMimo) && !d.isOpenRouter,
521
546
  allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !reasoningCapable) && !d.isXiaomiMimo,
547
+ // Keep the typed sparse override key present for DeepSeek proxy replay.
548
+ syntheticReasoningContentFallback: undefined,
522
549
  replayReasoningContent: d.isLocalOpenAICompatBackend,
523
550
  qwenPreserveThinking:
524
551
  (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && d.isLocalOpenAICompatBackend,
@@ -751,6 +778,7 @@ function resolveOpenAIResponsesPolicy(
751
778
  reasoningCapable,
752
779
  requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,
753
780
  allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,
781
+ syntheticReasoningContentFallback: undefined,
754
782
  replayReasoningContent: false,
755
783
  qwenPreserveThinking: false,
756
784
  qwenTemplateReasoningEffort: false,
@@ -853,7 +881,9 @@ function resolveAnthropicPolicy(
853
881
  const isCopilot = modelMatchesHost(spec, "githubCopilot");
854
882
  const isZenmux = modelMatchesHost(spec, "zenmux");
855
883
  const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && facts.kimiMandatoryThinking;
856
- const isAzure = isAzureAnthropicRoute(baseUrl);
884
+ const bedrockMessagesApi = isBedrockAnthropicRoute(baseUrl);
885
+ // Both routes reject the top-level tool `strict` field.
886
+ const rejectsStrictTools = isAzureAnthropicRoute(baseUrl) || bedrockMessagesApi;
857
887
  const signingEndpoint = official || isCopilot || isZenmux || isAnthropicSigningProxyUrl(baseUrl);
858
888
  const compat: ResolvedAnthropicCompat = {
859
889
  officialEndpoint: official,
@@ -862,7 +892,9 @@ function resolveAnthropicPolicy(
862
892
  supportsServerCompaction: false,
863
893
  firstPartyProvider: false,
864
894
  supportsOutputEffort: true,
865
- disableStrictTools: isAzure,
895
+ disableStrictTools: rejectsStrictTools,
896
+ // Present as a key so models.yml `compat` can set it; unset unless detected.
897
+ bedrockMessagesApi: bedrockMessagesApi || undefined,
866
898
  disableAdaptiveThinking: false,
867
899
  allowAnthropicHeaderOverrides: false,
868
900
  supportsEagerToolInputStreaming: official,
@@ -872,6 +904,7 @@ function resolveAnthropicPolicy(
872
904
  supportsMidConversationToolChanges: false,
873
905
  supportsPerMessageEffort: false,
874
906
  supportsThinkingBindingControls: false,
907
+ supportsBetweenToolsThinking: false,
875
908
  supportsForcedToolChoice: !requiresThinkingEnabled && !facts.family("fable", "mythos"),
876
909
  supportsSamplingParams: !facts.anthropicAdaptiveGenAtLeast("4.7"),
877
910
  requiresToolResultId: false,
@@ -882,6 +915,9 @@ function resolveAnthropicPolicy(
882
915
  injectClaudeCodeInstruction: true,
883
916
  stripImageInput: false,
884
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,
885
921
  streamIdleTimeoutMs: spec.compat?.streamIdleTimeoutMs,
886
922
  };
887
923
  applyWireAxes(compat, axes.wire, "anthropic-messages");
@@ -1197,7 +1233,15 @@ function fillExplicitThinking<TApi extends Api>(
1197
1233
  (impliesMandatoryReasoning(facts, spec.id) || isQwenTemplateReasoningEffortCompat(compat)));
1198
1234
  const needsDefaultLevel = thinking.defaultLevel === undefined && rule.defaultLevel !== undefined;
1199
1235
  const needsPrefixBinding = thinking.prefixBinding === undefined && rule.prefixBinding === true;
1200
- if (effortMap === undefined && !needsDisplay && !needsRequiresEffort && !needsDefaultLevel && !needsPrefixBinding) {
1236
+ const needsEffortBudgets = thinking.effortBudgets === undefined && rule.effortBudgets !== undefined;
1237
+ if (
1238
+ effortMap === undefined &&
1239
+ !needsDisplay &&
1240
+ !needsRequiresEffort &&
1241
+ !needsDefaultLevel &&
1242
+ !needsPrefixBinding &&
1243
+ !needsEffortBudgets
1244
+ ) {
1201
1245
  return thinking;
1202
1246
  }
1203
1247
  const filled: ThinkingConfig = { ...thinking };
@@ -1206,6 +1250,7 @@ function fillExplicitThinking<TApi extends Api>(
1206
1250
  if (needsDefaultLevel && rule.defaultLevel !== undefined) filled.defaultLevel = rule.defaultLevel;
1207
1251
  if (needsRequiresEffort) filled.requiresEffort = true;
1208
1252
  if (needsPrefixBinding) filled.prefixBinding = true;
1253
+ if (needsEffortBudgets) filled.effortBudgets = rule.effortBudgets;
1209
1254
  return filled;
1210
1255
  }
1211
1256
 
@@ -1254,11 +1299,16 @@ export function resolveCatalogAxes(spec: ModelSpec<Api>): Record<string, unknown
1254
1299
  * Resolves the full policy surface for one model spec: structured identity,
1255
1300
  * complete compat record, thinking metadata, and catalog-data corrections.
1256
1301
  */
1257
- export function resolveModelPolicy<TApi extends Api>(spec: ModelSpec<TApi>): ResolvedModelPolicy<TApi>;
1258
- export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Api> {
1302
+ export function resolveModelPolicy<TApi extends Api>(
1303
+ spec: ModelSpec<TApi>,
1304
+ route?: ResolveRoute,
1305
+ ): ResolvedModelPolicy<TApi>;
1306
+ export function resolveModelPolicy(spec: ModelSpec<Api>, route?: ResolveRoute): ResolvedModelPolicy<Api> {
1259
1307
  const identity = resolveIdentity(spec);
1260
1308
  const facts = new IdentityFacts(identity);
1261
- const axes = resolveCascade(buildResolveTarget(spec, identity));
1309
+ const target = buildResolveTarget(spec, identity);
1310
+ target.upstream = route?.upstream;
1311
+ const axes = resolveCascade(target);
1262
1312
  let compat: CompatOf<Api>;
1263
1313
  if (specUsesApi(spec, "openrouter")) {
1264
1314
  const chat = resolveOpenAICompletionsPolicy(spec, facts, axes);
@@ -1292,6 +1342,7 @@ export function resolveModelPolicy(spec: ModelSpec<Api>): ResolvedModelPolicy<Ap
1292
1342
  compat,
1293
1343
  thinking: resolveThinkingPolicy(spec, facts, axes, compat),
1294
1344
  catalog: axes.catalog,
1345
+ request: resolveRequestPolicy(axes),
1295
1346
  };
1296
1347
  }
1297
1348