@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.
- package/CHANGELOG.md +31 -0
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/compat/axes.d.ts +3 -1
- package/dist/types/compat/factory-droid.d.ts +65 -0
- package/dist/types/compat/provider-ids.d.ts +1 -1
- package/dist/types/compat/resolve.d.ts +8 -2
- package/dist/types/compat/types.d.ts +26 -1
- package/dist/types/discovery/cursor-proto.d.ts +284 -0
- package/dist/types/discovery/cursor.d.ts +12 -6
- package/dist/types/discovery/factory-droid.d.ts +28 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/protobuf.d.ts +3 -3
- package/dist/types/model-manager.d.ts +10 -0
- package/dist/types/models.d.ts +5 -1
- package/dist/types/provider-models/cache-provider-id.d.ts +2 -1
- package/dist/types/provider-models/descriptor-types.d.ts +2 -1
- package/dist/types/provider-models/openai-compat.d.ts +21 -0
- package/dist/types/provider-models/special.d.ts +2 -0
- package/dist/types/types.d.ts +100 -3
- package/dist/types/wire/cursor.d.ts +20 -0
- package/dist/types/wire/factory-droid.d.ts +45 -0
- package/package.json +4 -4
- package/src/build.ts +21 -0
- package/src/compat/auth-ids.ts +4 -0
- package/src/compat/axes.ts +113 -2
- package/src/compat/cascade.ts +8 -1
- package/src/compat/collapse.ts +3 -0
- package/src/compat/factory-droid.ts +170 -0
- package/src/compat/provider-ids.ts +2 -0
- package/src/compat/resolve.ts +47 -5
- package/src/compat/rules/README.md +18 -8
- package/src/compat/rules/auth/_order.kdl +2 -2
- package/src/compat/rules/auth/factory-droid.kdl +28 -0
- package/src/compat/rules/auth/helmcode.kdl +9 -0
- package/src/compat/rules/classes/anthropic.kdl +195 -80
- package/src/compat/rules/providers/commandcode.kdl +21 -5
- package/src/compat/rules/providers/cursor.kdl +29 -17
- package/src/compat/rules/providers/factory-droid.kdl +1166 -0
- package/src/compat/rules/providers/helmcode.kdl +120 -0
- package/src/compat/rules/runtime/behavior.kdl +65 -4
- package/src/compat/rules/taxonomy/_collapse.kdl +36 -14
- package/src/compat/rules.json +1 -1
- package/src/compat/types.ts +27 -1
- package/src/discovery/cursor-models.proto +261 -0
- package/src/discovery/cursor-proto.ts +471 -0
- package/src/discovery/cursor.ts +817 -88
- package/src/discovery/devin.ts +1 -3
- package/src/discovery/factory-droid.ts +296 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/protobuf.ts +16 -7
- package/src/model-manager.ts +13 -0
- package/src/models.json +1 -1
- package/src/models.ts +13 -0
- package/src/provider-models/cache-provider-id.ts +37 -6
- package/src/provider-models/descriptor-types.ts +2 -1
- package/src/provider-models/descriptors.ts +4 -0
- package/src/provider-models/openai-compat.ts +83 -17
- package/src/provider-models/special.ts +26 -2
- package/src/types.ts +112 -1
- package/src/wire/cursor.ts +32 -0
- package/src/wire/factory-droid.ts +130 -0
package/src/compat/axes.ts
CHANGED
|
@@ -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. */
|
package/src/compat/cascade.ts
CHANGED
|
@@ -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) +
|
package/src/compat/collapse.ts
CHANGED
|
@@ -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"
|
package/src/compat/resolve.ts
CHANGED
|
@@ -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
|
-
|
|
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>(
|
|
1268
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|