mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
- stripCatalogPrefix boundary-aligned: customgpt/sgpt/mygpt no longer false-strip to gpt-* forms; boundary check requires position 0 or non-alphanumeric precursor - New match_kind gpt-version-segment: gpt-neox-20b routes mlflow-chat (was residual collision); gpt segment match requires next segment to start with digit or be dashless gpt5 form - PROVIDER_FALLBACKS Record→Map: constructor/__proto__ prototype-key leak closed; resolveModelCapabilities/getProviderEffortConfig return complete records for all provider strings - Corpus 69→77 vectors, all 6-axis mandatory; JS and Rust runners hard-fail on missing axes; prototype-key vectors added; boundary-negative vectors added (sgpt-5-5, mygpt-5, customgpt-5-5-endpoint all route mlflow-chat); gpt-neox-20b pinned as mlflow-chat (corrected behavior, not residual collision) - Manifest validator 33→42 tests: family match_priority integer guard, lowercase duplicate-key detection, post-inheritance materialized default∈supported check, assertEfforts shared helper covering family/fallback/exact - Clippy: map_or(false)→is_some_and in emitter template; fmt clean - Desktop regression tests: constructor/__proto__ prototype-key stability Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
1702 lines
68 KiB
JavaScript
1702 lines
68 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Model-capability manifest generator.
|
|
*
|
|
* Reads `scripts/model-capabilities.json` and emits:
|
|
* - `crates/buzz-agent/src/generated_model_capabilities.rs`
|
|
* - `desktop/src/features/agents/ui/modelCapabilities.ts`
|
|
*
|
|
* The generator performs three ordered resolution steps (resolver contract, plan v4):
|
|
* 1. Provider-qualified raw exact lookup — key is (provider, raw_model_id), matched
|
|
* BEFORE any prefix stripping or normalization.
|
|
* 2. Provider-scoped ordered family rules — match kinds: exact | prefix | gpt5-token |
|
|
* gpt5-base | segment | segment-prefix, ordered by match_priority (desc).
|
|
* 3. Per-axis provider fallback — blank vs concrete-unknown states.
|
|
*
|
|
* Completeness validator: every emitted CapabilityResult must have every axis populated.
|
|
* A generation-time failure on any unresolvable axis is a hard error.
|
|
*
|
|
* Usage:
|
|
* node scripts/generate-model-capabilities.mjs [--check]
|
|
*
|
|
* --check: verify that already-generated files match what the generator would produce
|
|
* (used by CI regenerate-then-diff job). Exits 1 if any file differs.
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = join(__dirname, "..");
|
|
const CHECK_MODE = process.argv.includes("--check");
|
|
const CHECK_GOOSE_MODE = process.argv.includes("--check-goose");
|
|
|
|
// Run content through rustfmt (stdin → stdout) if available; fall back to raw.
|
|
// Uses hermit-pinned rustfmt from bin/ when present (same binary as pre-commit hook).
|
|
function rustfmt(content) {
|
|
const hermitBin = join(repoRoot, "bin", "rustfmt");
|
|
const hermitExists = existsSync(hermitBin);
|
|
const rustfmtBin = hermitExists ? hermitBin : "rustfmt";
|
|
const result = spawnSync(rustfmtBin, ["--edition", "2021", "--emit", "stdout"], {
|
|
input: content,
|
|
encoding: "utf8",
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
if (result.status === 0 && result.stdout) return result.stdout;
|
|
if (hermitExists && result.status !== 0) {
|
|
// Hermit-pinned binary is present but failed — fail closed so CI catches drift.
|
|
const stderr = result.stderr ?? "";
|
|
throw new Error(`rustfmt (hermit-pinned) exited ${result.status}: ${stderr.trim()}`);
|
|
}
|
|
// rustfmt not available (PATH fallback also absent) — warn and return raw.
|
|
// CI's regen-diff gate will catch any drift.
|
|
process.stderr.write("warning: rustfmt not available; raw Rust output may not be formatter-stable\n");
|
|
return content;
|
|
}
|
|
|
|
// Support --manifest-path and --output-dir flags for testing
|
|
const manifestPathOverride = (() => {
|
|
const idx = process.argv.indexOf("--manifest-path");
|
|
return idx !== -1 ? process.argv[idx + 1] : null;
|
|
})();
|
|
const outputDirOverride = (() => {
|
|
const idx = process.argv.indexOf("--output-dir");
|
|
return idx !== -1 ? process.argv[idx + 1] : null;
|
|
})();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Load manifest
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const manifestPath = manifestPathOverride ?? join(repoRoot, "scripts", "model-capabilities.json");
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
|
|
// Validate registry_labels — must be an array of {id, label} objects with unique IDs.
|
|
// JSON.parse() silently overwrites duplicate object keys, so an array is required for
|
|
// structural duplicate detection.
|
|
const registryLabelsArr = manifest.registry_labels ?? [];
|
|
if (!Array.isArray(registryLabelsArr)) {
|
|
throw new Error("registry_labels: must be an array of {id, label} objects");
|
|
}
|
|
for (const entry of registryLabelsArr) {
|
|
if (!entry.id || typeof entry.id !== "string" || entry.id.trim() === "") {
|
|
throw new Error(`registry_labels: entry missing nonempty string "id": ${JSON.stringify(entry)}`);
|
|
}
|
|
if (!entry.label || typeof entry.label !== "string" || entry.label.trim() === "") {
|
|
throw new Error(`registry_labels: entry id="${entry.id}" missing nonempty string "label"`);
|
|
}
|
|
// Safe for code interpolation: reject double-quote, backslash, and control chars.
|
|
// (These characters would break emitted Rust/TS string literals.)
|
|
// Checked via requireSafeString() below (shared validator defined after knownModels block).
|
|
const unsafeId = entry.id.includes('"') || entry.id.includes("\\") ||
|
|
Array.from(entry.id).some((c) => c.charCodeAt(0) < 32);
|
|
if (unsafeId) {
|
|
throw new Error(`registry_labels: entry id="${entry.id}" contains unsafe characters`);
|
|
}
|
|
const unsafeLabel = entry.label.includes('"') || entry.label.includes("\\") ||
|
|
Array.from(entry.label).some((c) => c.charCodeAt(0) < 32);
|
|
if (unsafeLabel) {
|
|
throw new Error(`registry_labels: entry id="${entry.id}" label contains unsafe characters`);
|
|
}
|
|
}
|
|
const registryLabelIds = registryLabelsArr.map((e) => e.id);
|
|
const registryLabelSet = new Set(registryLabelIds);
|
|
if (registryLabelSet.size !== registryLabelIds.length) {
|
|
const dups = registryLabelIds.filter((id, i) => registryLabelIds.indexOf(id) !== i);
|
|
throw new Error(`registry_labels: duplicate endpoint IDs detected: ${dups.join(", ")}`);
|
|
}
|
|
|
|
// Validate databricks_v2_known_models uniqueness
|
|
const knownModels = manifest.databricks_v2_known_models ?? [];
|
|
const knownModelsSet = new Set(knownModels);
|
|
if (knownModelsSet.size !== knownModels.length) {
|
|
throw new Error(`databricks_v2_known_models: duplicate IDs detected: ${
|
|
knownModels.filter((id, i) => knownModels.indexOf(id) !== i).join(", ")
|
|
}`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared string-safety validator
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// All manifest strings that end up inside generated Rust or TypeScript string literals
|
|
// must not contain double-quote, backslash, or control characters — any of these would
|
|
// break the emitted source or allow injection. One shared check prevents the same class
|
|
// of defect from appearing at scattered emission sites.
|
|
//
|
|
// Usage: requireSafeString(value, "context.path") — throws on violation.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function requireSafeString(value, context) {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
throw new Error(`${context}: must be a nonempty string, got ${JSON.stringify(value)}`);
|
|
}
|
|
const hasUnsafe = value.includes('"') || value.includes("\\") ||
|
|
Array.from(value).some((c) => c.charCodeAt(0) < 32);
|
|
if (hasUnsafe) {
|
|
throw new Error(`${context}: contains unsafe characters (double-quote, backslash, or control chars): ${JSON.stringify(value)}`);
|
|
}
|
|
}
|
|
|
|
// Validate all manifest strings that end up in generated Rust/TS string literals.
|
|
// family_tokens → Rust FAMILY_TOKENS array and TS FAMILY_TOKENS constant
|
|
for (const tok of manifest.family_tokens ?? []) {
|
|
requireSafeString(tok, "family_tokens[]");
|
|
}
|
|
|
|
// databricks_v2_known_models IDs → Rust/TS DATABRICKS_V2_KNOWN_MODELS array
|
|
for (const id of knownModels) {
|
|
requireSafeString(id, "databricks_v2_known_models[]");
|
|
}
|
|
|
|
// exact_records → Rust lookup_exact() match arms and TS EXACT_RECORDS map keys
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
requireSafeString(rec.provider ?? "", `exact_records[${rec.raw_model_id}].provider`);
|
|
requireSafeString(rec.raw_model_id ?? "", `exact_records[${rec.raw_model_id}].raw_model_id`);
|
|
}
|
|
|
|
// family_rules → Rust/TS lookup_by_family_rules() match-expression strings
|
|
for (const rule of manifest.family_rules ?? []) {
|
|
requireSafeString(rule.id ?? "", `family_rules[${rule.id}].id`);
|
|
requireSafeString(rule.match_value ?? "", `family_rules[${rule.id}].match_value`);
|
|
for (const alias of rule.match_aliases ?? []) {
|
|
requireSafeString(alias, `family_rules[${rule.id}].match_aliases[]`);
|
|
}
|
|
for (const provider of rule.providers ?? []) {
|
|
requireSafeString(provider, `family_rules[${rule.id}].providers[]`);
|
|
}
|
|
// registry_label flows into Rust Some("...") and TS "..." string literals
|
|
if (rule.registry_label != null) {
|
|
requireSafeString(rule.registry_label, `family_rules[${rule.id}].registry_label`);
|
|
}
|
|
}
|
|
|
|
// exact_records: registry_label values flow into Rust rustString() and TS emitter
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
if (rec.registry_label != null) {
|
|
requireSafeString(rec.registry_label, `exact_records[${rec.raw_model_id}].registry_label`);
|
|
}
|
|
}
|
|
|
|
// provider_fallbacks: object keys are interpolated as Rust match arms and TS object keys
|
|
for (const providerKey of Object.keys(manifest.provider_fallbacks ?? {})) {
|
|
if (providerKey !== "_default") {
|
|
requireSafeString(providerKey, `provider_fallbacks key`);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// --check-goose: optional drift check against pinned goose upstream
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Reads the goose source file at the pinned revision and verifies that
|
|
// manifest.databricks_v2_known_models exactly matches the IDs declared there.
|
|
//
|
|
// Usage (non-CI, opt-in):
|
|
// node scripts/generate-model-capabilities.mjs --check-goose
|
|
//
|
|
// Pin metadata lives in manifest._sources.goose_known_models:
|
|
// "goose revision <SHA> (<path>:<lines>) — <ids>"
|
|
//
|
|
// The check fetches the raw file from github.com/block/goose via the GitHub
|
|
// contents API and parses DATABRICKS_V2_KNOWN_MODELS from it.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
if (CHECK_GOOSE_MODE) {
|
|
// Parse pin metadata from _sources
|
|
const pin = manifest._sources?.goose_known_models;
|
|
if (!pin) {
|
|
console.error("--check-goose: manifest._sources.goose_known_models is missing.");
|
|
process.exit(1);
|
|
}
|
|
const revMatch = pin.match(/revision\s+([0-9a-f]{7,40})/i);
|
|
const pathMatch = pin.match(/\(([^)]+\.rs):/);
|
|
if (!revMatch || !pathMatch) {
|
|
console.error(`--check-goose: could not parse revision/path from pin metadata: ${pin}`);
|
|
process.exit(1);
|
|
}
|
|
const pinnedRev = revMatch[1];
|
|
const goosePath = pathMatch[1]; // e.g. "crates/goose-providers/src/databricks_v2.rs"
|
|
|
|
// Fetch file content from GitHub API (no token required for public repos)
|
|
const url = `https://raw.githubusercontent.com/block/goose/${pinnedRev}/${goosePath}`;
|
|
let fileContent;
|
|
try {
|
|
const result = spawnSync("curl", ["-fsS", "--max-time", "15", url], {
|
|
encoding: "utf8",
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
if (result.status !== 0) {
|
|
console.error(`--check-goose: curl failed (exit ${result.status}): ${result.stderr?.trim()}`);
|
|
console.error(` URL: ${url}`);
|
|
process.exit(1);
|
|
}
|
|
fileContent = result.stdout;
|
|
} catch (e) {
|
|
console.error(`--check-goose: fetch failed: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Extract DATABRICKS_V2_KNOWN_MODELS from Rust source: parse &[&str] literal
|
|
// Pattern: pub const DATABRICKS_V2_KNOWN_MODELS: &[&str] = &[ "id1", "id2", ... ];
|
|
const constMatch = fileContent.match(
|
|
/DATABRICKS_V2_KNOWN_MODELS\s*:\s*&\[&str\]\s*=\s*&\[([\s\S]*?)\];/,
|
|
);
|
|
if (!constMatch) {
|
|
console.error(`--check-goose: could not find DATABRICKS_V2_KNOWN_MODELS in goose source at ${pinnedRev}:${goosePath}`);
|
|
process.exit(1);
|
|
}
|
|
const gooseIds = [...constMatch[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort();
|
|
const manifestIds = [...(manifest.databricks_v2_known_models ?? [])].sort();
|
|
|
|
const onlyInGoose = gooseIds.filter((id) => !manifestIds.includes(id));
|
|
const onlyInManifest = manifestIds.filter((id) => !gooseIds.includes(id));
|
|
|
|
if (onlyInGoose.length === 0 && onlyInManifest.length === 0) {
|
|
console.log(`--check-goose: OK — manifest matches goose@${pinnedRev} (${gooseIds.length} IDs)`);
|
|
} else {
|
|
if (onlyInGoose.length > 0) {
|
|
console.error(`--check-goose: DRIFT — IDs in goose@${pinnedRev} not in manifest: ${onlyInGoose.join(", ")}`);
|
|
}
|
|
if (onlyInManifest.length > 0) {
|
|
console.error(`--check-goose: DRIFT — IDs in manifest not in goose@${pinnedRev}: ${onlyInManifest.join(", ")}`);
|
|
}
|
|
console.error(`Pin: ${pin}`);
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Validation helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const VALID_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
const VALID_THINKING_MODES = ["manual-budget", "adaptive", "omit-fields", "none", "not-applicable"];
|
|
const VALID_DBV2_ROUTES = [
|
|
"openai-responses",
|
|
"anthropic-messages",
|
|
"mlflow-chat",
|
|
"route-unknown",
|
|
"not-applicable",
|
|
];
|
|
const VALID_NORM_POLICIES = ["none", "openai-standard", "openai-clamp-max-to-xhigh"];
|
|
const VALID_MATCH_KINDS = [
|
|
"exact",
|
|
"prefix",
|
|
"gpt5-token",
|
|
"gpt5-base",
|
|
"segment",
|
|
"segment-prefix",
|
|
"gpt-version-segment",
|
|
];
|
|
|
|
function assertEnum(value, valid, label) {
|
|
if (!valid.includes(value)) {
|
|
throw new Error(`${label}: invalid value "${value}" (must be one of: ${valid.join(", ")})`);
|
|
}
|
|
}
|
|
|
|
function assertNonEmpty(arr, label) {
|
|
if (!Array.isArray(arr) || arr.length === 0) {
|
|
throw new Error(`${label}: must be a non-empty array`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shared effort-list validator: non-empty, all valid enum values, no duplicates,
|
|
* canonical sort order (Rust clamp logic depends on sorted order).
|
|
*/
|
|
function assertEfforts(efforts, label) {
|
|
assertNonEmpty(efforts, label);
|
|
const seen = new Set();
|
|
for (const e of efforts) {
|
|
assertEnum(e, VALID_EFFORTS, `${label}[]`);
|
|
if (seen.has(e)) throw new Error(`${label}: duplicate effort "${e}"`);
|
|
seen.add(e);
|
|
}
|
|
const indices = efforts.map((e) => VALID_EFFORTS.indexOf(e));
|
|
for (let i = 1; i < indices.length; i++) {
|
|
if (indices[i] <= indices[i - 1]) {
|
|
throw new Error(
|
|
`${label}: must follow canonical order [${VALID_EFFORTS.join(", ")}]; got [${efforts.join(", ")}]`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateFallbackRecord(rec, label) {
|
|
assertEnum(rec.databricks_v2_wire_route, VALID_DBV2_ROUTES, `${label}.databricks_v2_wire_route`);
|
|
assertEnum(rec.thinking_mode, VALID_THINKING_MODES, `${label}.thinking_mode`);
|
|
assertEfforts(rec.supported_efforts, `${label}.supported_efforts`);
|
|
if (rec.default_effort !== null) {
|
|
assertEnum(rec.default_effort, VALID_EFFORTS, `${label}.default_effort`);
|
|
if (!rec.supported_efforts.includes(rec.default_effort)) {
|
|
throw new Error(
|
|
`${label}: default_effort "${rec.default_effort}" not in supported_efforts [${rec.supported_efforts.join(", ")}]`,
|
|
);
|
|
}
|
|
}
|
|
assertEnum(rec.normalization_policy, VALID_NORM_POLICIES, `${label}.normalization_policy`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Validate manifest structure
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Validate family_rules
|
|
const seenRuleIds = new Set();
|
|
for (const rule of manifest.family_rules) {
|
|
if (!rule.id) throw new Error("family_rule missing id");
|
|
if (seenRuleIds.has(rule.id)) throw new Error(`duplicate family_rule id: ${rule.id}`);
|
|
seenRuleIds.add(rule.id);
|
|
assertEnum(rule.match_kind, VALID_MATCH_KINDS, `rule ${rule.id} match_kind`);
|
|
assertEnum(rule.thinking_mode, VALID_THINKING_MODES, `rule ${rule.id} thinking_mode`);
|
|
// match_priority must be a non-negative integer (interpolated into Rust comments and TS code)
|
|
if (!Number.isInteger(rule.match_priority) || rule.match_priority < 0) {
|
|
throw new Error(`rule ${rule.id}: match_priority must be a non-negative integer, got ${JSON.stringify(rule.match_priority)}`);
|
|
}
|
|
assertEfforts(rule.supported_efforts, `rule ${rule.id} supported_efforts`);
|
|
if (rule.default_effort !== null) {
|
|
assertEnum(rule.default_effort, VALID_EFFORTS, `rule ${rule.id} default_effort`);
|
|
if (!rule.supported_efforts.includes(rule.default_effort)) {
|
|
throw new Error(
|
|
`rule ${rule.id}: default_effort "${rule.default_effort}" not in supported_efforts`,
|
|
);
|
|
}
|
|
}
|
|
assertEnum(
|
|
rule.databricks_v2_wire_route,
|
|
VALID_DBV2_ROUTES,
|
|
`rule ${rule.id} databricks_v2_wire_route`,
|
|
);
|
|
assertEnum(
|
|
rule.normalization_policy,
|
|
VALID_NORM_POLICIES,
|
|
`rule ${rule.id} normalization_policy`,
|
|
);
|
|
}
|
|
|
|
// Validate provider_fallbacks
|
|
for (const [provider, fb] of Object.entries(manifest.provider_fallbacks)) {
|
|
for (const state of ["blank", "concrete_unknown"]) {
|
|
if (!fb[state])
|
|
throw new Error(`provider_fallbacks.${provider} missing "${state}" record`);
|
|
validateFallbackRecord(fb[state], `provider_fallbacks.${provider}.${state}`);
|
|
}
|
|
}
|
|
|
|
// Validate exact_records — full invariant checks on every override axis
|
|
// Keys are normalized to lowercase before duplicate detection to match build-time key lowercasing.
|
|
const seenExactKeys = new Set();
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
if (!rec.provider || !rec.raw_model_id)
|
|
throw new Error("exact_record missing provider or raw_model_id");
|
|
// Normalize to lowercase — both emitters lowercase provider+raw_model_id at build time
|
|
const key = `${rec.provider.toLowerCase()}::${rec.raw_model_id.toLowerCase()}`;
|
|
if (seenExactKeys.has(key)) throw new Error(`duplicate exact_record key: ${key}`);
|
|
seenExactKeys.add(key);
|
|
|
|
// Validate match_priority: must be a non-negative integer if present (interpolated into source).
|
|
if (rec.match_priority !== undefined) {
|
|
if (!Number.isInteger(rec.match_priority) || rec.match_priority < 0)
|
|
throw new Error(`exact_record ${key}: match_priority must be a non-negative integer`);
|
|
}
|
|
|
|
// Validate supported_efforts_override if present (shared validator: non-empty, no dupes, canonical order).
|
|
if (rec.supported_efforts_override !== undefined) {
|
|
assertEfforts(rec.supported_efforts_override, `exact_record ${key} supported_efforts_override`);
|
|
// Validate default_effort is in the override if present.
|
|
if (rec.default_effort !== undefined && rec.default_effort !== null) {
|
|
assertEnum(rec.default_effort, VALID_EFFORTS, `exact_record ${key} default_effort`);
|
|
if (!rec.supported_efforts_override.includes(rec.default_effort)) {
|
|
throw new Error(
|
|
`exact_record ${key}: default_effort "${rec.default_effort}" not in supported_efforts_override [${rec.supported_efforts_override.join(", ")}]`,
|
|
);
|
|
}
|
|
}
|
|
} else if (rec.default_effort !== undefined && rec.default_effort !== null) {
|
|
// default_effort override without supported_efforts_override — still validate enum.
|
|
assertEnum(rec.default_effort, VALID_EFFORTS, `exact_record ${key} default_effort`);
|
|
}
|
|
|
|
// Validate thinking_mode override if present.
|
|
if (rec.thinking_mode !== undefined) {
|
|
assertEnum(rec.thinking_mode, VALID_THINKING_MODES, `exact_record ${key} thinking_mode`);
|
|
}
|
|
|
|
// Validate databricks_v2_wire_route override if present.
|
|
if (rec.databricks_v2_wire_route !== undefined) {
|
|
assertEnum(rec.databricks_v2_wire_route, VALID_DBV2_ROUTES, `exact_record ${key} databricks_v2_wire_route`);
|
|
}
|
|
|
|
// Validate normalization_policy override if present.
|
|
if (rec.normalization_policy !== undefined) {
|
|
assertEnum(rec.normalization_policy, VALID_NORM_POLICIES, `exact_record ${key} normalization_policy`);
|
|
}
|
|
}
|
|
|
|
// Post-inheritance materialized validation for exact_records.
|
|
// An exact_record with no supported_efforts_override inherits the family default — validate
|
|
// that the final materialized default_effort is within the final materialized supported_efforts.
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
const key = `${rec.provider.toLowerCase()}::${rec.raw_model_id.toLowerCase()}`;
|
|
const materializedResult = resolve(rec.provider, rec.raw_model_id);
|
|
const matEfforts = materializedResult.supported_efforts;
|
|
const matDefault = materializedResult.default_effort;
|
|
if (matDefault !== undefined && matDefault !== null) {
|
|
if (!matEfforts.includes(matDefault)) {
|
|
throw new Error(
|
|
`exact_record ${key}: materialized default_effort "${matDefault}" not in materialized supported_efforts [${matEfforts.join(", ")}] (check family default inheritance)`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resolution engine (mirrors plan resolver contract)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Strip catalog prefix to get the normalized alias for family-rule matching.
|
|
* Finds the first boundary-aligned occurrence of a known family token and returns from there.
|
|
* Boundary-aligned: the token must start at position 0 or be preceded by a non-alphanumeric char.
|
|
* This prevents "customgpt-5-5-endpoint" from stripping to "gpt-5-5-endpoint" via "gpt-" inside
|
|
* the "customgpt-" prefix.
|
|
* e.g. "goose-claude-fable-5" → "claude-fable-5" (boundary: preceded by "-")
|
|
* "databricks-gpt-5.5" → "gpt-5.5" (boundary: preceded by "-")
|
|
* "claude-opus-4-7" → "claude-opus-4-7" (no prefix, already at boundary)
|
|
* "customgpt-5-5-ep" → "customgpt-5-5-ep" (no boundary match: 'g' preceded by 'm')
|
|
*/
|
|
function stripCatalogPrefix(model) {
|
|
const lower = model.toLowerCase();
|
|
let firstIdx = Infinity;
|
|
for (const tok of manifest.family_tokens) {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = lower.indexOf(tok, start);
|
|
if (idx === -1) break;
|
|
// Boundary check: position 0 or preceded by a non-alphanumeric character
|
|
if (idx === 0 || !/[a-z0-9]/.test(lower[idx - 1])) {
|
|
if (idx < firstIdx) firstIdx = idx;
|
|
break;
|
|
}
|
|
start = idx + 1;
|
|
}
|
|
}
|
|
return firstIdx === Infinity ? model : model.slice(firstIdx);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the character at position idx-1 in str is a valid left boundary:
|
|
* start-of-string, "-", or ".". Prevents substring matches like "customgpt" matching "gpt".
|
|
*/
|
|
function hasLeftBoundary(str, idx) {
|
|
if (idx === 0) return true;
|
|
const prev = str[idx - 1];
|
|
return prev === "-" || prev === ".";
|
|
}
|
|
|
|
/**
|
|
* gpt5-token match: model contains token at a word boundary (end-of-string or "-").
|
|
* Does NOT match if followed by a digit or letter.
|
|
* Left-boundary checked: token must start at beginning of string or after "-" or ".".
|
|
*/
|
|
function gpt5TokenMatches(model, token) {
|
|
const lower = model.toLowerCase();
|
|
const tok = token.toLowerCase();
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = lower.indexOf(tok, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + tok.length;
|
|
if (!hasLeftBoundary(lower, idx)) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
const afterChar = afterIdx < lower.length ? lower[afterIdx] : "";
|
|
if (afterChar === "" || afterChar === "-") return true;
|
|
start = afterIdx;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* gpt5-base match: like gpt5-token but also rejects short -<1-3 digit> suffixes.
|
|
* Left-boundary checked: token must start at beginning of string or after "-" or ".".
|
|
*/
|
|
function gpt5BaseMatches(model, token) {
|
|
const lower = model.toLowerCase();
|
|
const tok = token.toLowerCase();
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = lower.indexOf(tok, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + tok.length;
|
|
if (!hasLeftBoundary(lower, idx)) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
const suffix = lower.slice(afterIdx);
|
|
if (suffix === "") return true;
|
|
if (!suffix.startsWith("-")) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
const dashRest = suffix.slice(1);
|
|
if (/^\d{1,3}(?:[^a-z\d]|$)/i.test(dashRest)) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* gpt-version-segment match: the token appears as an exact segment AND the next segment
|
|
* (the one immediately after the token) starts with a digit.
|
|
* This matches "gpt" in "gpt-5.5" (next seg "5") and "gpt" in "gpt-5-4-mini" (next seg "5")
|
|
* but NOT "gpt" in "gpt-neox-20b" (next seg "neox" starts with a letter).
|
|
* Also matches the dashless exact-segment alias (e.g. "gpt5") directly via segment equality.
|
|
*/
|
|
function gptVersionSegmentMatches(model, token) {
|
|
const lower = model.toLowerCase();
|
|
const tok = token.toLowerCase();
|
|
const segs = lower.split(/[^a-z0-9]+/);
|
|
for (let i = 0; i < segs.length; i++) {
|
|
if (segs[i] === tok) {
|
|
// Exact segment match (e.g. "gpt5" matches "gpt5-custom")
|
|
if (tok.length > 3 || /^\d/.test(tok)) return true; // dashless numeric form like "gpt5"
|
|
// For "gpt": require the next segment to start with a digit
|
|
const nextSeg = segs[i + 1];
|
|
if (nextSeg !== undefined && /^\d/.test(nextSeg)) return true;
|
|
// No valid next segment — this "gpt" segment alone does not match
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Test if a family rule matches the given (normalized) model string for a provider.
|
|
*/
|
|
function ruleMatchesModel(rule, normalizedModel, provider) {
|
|
if (!rule.providers.includes(provider)) return false;
|
|
const lower = normalizedModel.toLowerCase();
|
|
const allTokens = [rule.match_value, ...(rule.match_aliases ?? [])];
|
|
switch (rule.match_kind) {
|
|
case "exact":
|
|
return allTokens.some((t) => lower === t.toLowerCase());
|
|
case "prefix":
|
|
return allTokens.some((t) => lower.startsWith(t.toLowerCase()));
|
|
case "gpt5-token":
|
|
return allTokens.some((t) => gpt5TokenMatches(lower, t));
|
|
case "gpt5-base":
|
|
return allTokens.some((t) => gpt5BaseMatches(lower, t));
|
|
case "segment": {
|
|
const segs = lower.split(/[^a-z0-9]+/);
|
|
return allTokens.some((t) => segs.includes(t.toLowerCase()));
|
|
}
|
|
case "segment-prefix": {
|
|
const segs = lower.split(/[^a-z0-9]+/);
|
|
return allTokens.some((t) => segs.some((s) => s.startsWith(t.toLowerCase())));
|
|
}
|
|
case "gpt-version-segment":
|
|
return allTokens.some((t) => gptVersionSegmentMatches(lower, t));
|
|
default:
|
|
throw new Error(`unknown match_kind: ${rule.match_kind}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve (provider, rawModelId) → CapabilityResult.
|
|
* Three steps: raw exact lookup → family rules → provider fallback.
|
|
*/
|
|
function resolve(provider, rawModelId) {
|
|
const isBlank = !rawModelId || rawModelId.trim() === "";
|
|
|
|
// Step 1: provider-qualified raw exact lookup (BEFORE any prefix stripping)
|
|
const exactRecord = (manifest.exact_records ?? []).find(
|
|
(r) => r.provider === provider && r.raw_model_id === rawModelId,
|
|
);
|
|
|
|
if (exactRecord) {
|
|
// Materialize the full record by evaluating family rules for any axis not overridden.
|
|
const normalizedAlias = stripCatalogPrefix(rawModelId);
|
|
const familyResult = resolveFamilyRules(provider, normalizedAlias, rawModelId);
|
|
const base = familyResult ?? getProviderFallback(provider, isBlank);
|
|
// Determine which axes came from family rule vs exact record for per-axis provenance.
|
|
const effortsFromExact = exactRecord.supported_efforts_override !== undefined;
|
|
const routeFromExact = exactRecord.databricks_v2_wire_route !== undefined;
|
|
const modeFromExact = exactRecord.thinking_mode !== undefined;
|
|
const normFromExact = exactRecord.normalization_policy !== undefined;
|
|
const effortDefaultFromExact = exactRecord.default_effort !== undefined;
|
|
const labelFromExact = exactRecord.registry_label !== undefined;
|
|
const familyProv = familyResult
|
|
? { rule_id: familyResult._provenance.rule_id, rule_priority: familyResult._provenance.rule_priority }
|
|
: null;
|
|
return {
|
|
registry_label: exactRecord.registry_label ?? null,
|
|
thinking_mode: exactRecord.thinking_mode ?? base.thinking_mode,
|
|
supported_efforts: exactRecord.supported_efforts_override ?? base.supported_efforts,
|
|
default_effort: exactRecord.default_effort !== undefined ? exactRecord.default_effort : base.default_effort,
|
|
databricks_v2_wire_route: exactRecord.databricks_v2_wire_route ?? base.databricks_v2_wire_route,
|
|
normalization_policy: exactRecord.normalization_policy ?? base.normalization_policy,
|
|
_provenance: {
|
|
source: "exact",
|
|
exact_key: `${provider}::${rawModelId}`,
|
|
registry_label: labelFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "absent"),
|
|
supported_efforts: effortsFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "fallback"),
|
|
databricks_v2_wire_route: routeFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "fallback"),
|
|
thinking_mode: modeFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "fallback"),
|
|
normalization_policy: normFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "fallback"),
|
|
default_effort: effortDefaultFromExact ? "exact_record" : (familyProv ? `family:${familyProv.rule_id}@${familyProv.rule_priority}` : "fallback"),
|
|
},
|
|
};
|
|
}
|
|
|
|
// Step 2: provider-scoped family rules on normalized alias
|
|
const normalizedAlias = stripCatalogPrefix(rawModelId ?? "");
|
|
const familyResult = resolveFamilyRules(provider, normalizedAlias, rawModelId);
|
|
if (familyResult) return familyResult;
|
|
|
|
// Step 3: provider fallback
|
|
const fallback = getProviderFallback(provider, isBlank);
|
|
return { ...fallback, registry_label: null };
|
|
}
|
|
|
|
function resolveFamilyRules(provider, normalizedAlias, rawModelId) {
|
|
if (!normalizedAlias) return null;
|
|
|
|
// Sort rules by match_priority descending (higher priority wins)
|
|
const sorted = [...manifest.family_rules].sort((a, b) => b.match_priority - a.match_priority);
|
|
|
|
for (const rule of sorted) {
|
|
if (ruleMatchesModel(rule, normalizedAlias, provider)) {
|
|
// databricks_v2_wire_route is only meaningful for databricks_v2; all other providers get not-applicable
|
|
const wireRoute = provider === "databricks_v2"
|
|
? rule.databricks_v2_wire_route
|
|
: "not-applicable";
|
|
return {
|
|
registry_label: rule.registry_label ?? null,
|
|
thinking_mode: rule.thinking_mode,
|
|
supported_efforts: rule.supported_efforts,
|
|
default_effort: rule.default_effort,
|
|
databricks_v2_wire_route: wireRoute,
|
|
normalization_policy: rule.normalization_policy,
|
|
_provenance: {
|
|
source: "family",
|
|
rule_id: rule.id,
|
|
rule_priority: rule.match_priority,
|
|
normalized_alias: normalizedAlias,
|
|
raw_model_id: rawModelId,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getProviderFallback(provider, isBlank) {
|
|
const fb =
|
|
manifest.provider_fallbacks[provider] ?? manifest.provider_fallbacks["_default"];
|
|
const state = isBlank ? "blank" : "concrete_unknown";
|
|
const rec = fb[state];
|
|
return {
|
|
...rec,
|
|
_provenance: { source: "fallback", provider, state },
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rust code generation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function rustString(s) {
|
|
if (s === null || s === undefined) return "None";
|
|
return `Some("${s}")`;
|
|
}
|
|
|
|
function rustEffortList(efforts) {
|
|
const mapped = efforts.map((e) => `ThinkingEffort::${capitalize(e)}`);
|
|
return `&[${mapped.join(", ")}]`;
|
|
}
|
|
|
|
function rustEffortOption(e) {
|
|
if (e === null || e === undefined) return "None";
|
|
return `Some(ThinkingEffort::${capitalize(e)})`;
|
|
}
|
|
|
|
function capitalize(s) {
|
|
if (s === "xhigh") return "XHigh";
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
function rustDbv2Route(route) {
|
|
const map = {
|
|
"openai-responses": "DatabricksV2Route::OpenAiResponses",
|
|
"anthropic-messages": "DatabricksV2Route::AnthropicMessages",
|
|
"mlflow-chat": "DatabricksV2Route::MlflowChatCompletions",
|
|
"route-unknown": "DatabricksV2Route::RouteUnknown",
|
|
"not-applicable": "DatabricksV2Route::NotApplicable",
|
|
};
|
|
if (!map[route]) throw new Error(`unknown dbv2 route: ${route}`);
|
|
return map[route];
|
|
}
|
|
|
|
function rustNormPolicy(policy) {
|
|
const map = {
|
|
none: "NormalizationPolicy::None",
|
|
"openai-standard": "NormalizationPolicy::OpenAiStandard",
|
|
"openai-clamp-max-to-xhigh": "NormalizationPolicy::OpenAiClampMaxToXHigh",
|
|
};
|
|
if (!map[policy]) throw new Error(`unknown norm policy: ${policy}`);
|
|
return map[policy];
|
|
}
|
|
|
|
function rustThinkingMode(mode) {
|
|
const map = {
|
|
"manual-budget": "ThinkingMode::ManualBudget",
|
|
adaptive: "ThinkingMode::Adaptive",
|
|
"omit-fields": "ThinkingMode::OmitFields",
|
|
none: "ThinkingMode::None",
|
|
"not-applicable": "ThinkingMode::NotApplicable",
|
|
};
|
|
if (!map[mode]) throw new Error(`unknown thinking mode: ${mode}`);
|
|
return map[mode];
|
|
}
|
|
|
|
function emitRustCapabilityResult(r, indent = " ") {
|
|
const i = indent;
|
|
const lines = [
|
|
`${i}CapabilityResult {`,
|
|
`${i} registry_label: ${rustString(r.registry_label)},`,
|
|
`${i} thinking_mode: ${rustThinkingMode(r.thinking_mode)},`,
|
|
`${i} supported_efforts: Cow::Borrowed(${rustEffortList(r.supported_efforts)}),`,
|
|
`${i} default_effort: ${rustEffortOption(r.default_effort)},`,
|
|
`${i} databricks_v2_wire_route: ${rustDbv2Route(r.databricks_v2_wire_route)},`,
|
|
`${i} normalization_policy: ${rustNormPolicy(r.normalization_policy)},`,
|
|
`${i}}`,
|
|
];
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// Build the exact_records map entries for Rust
|
|
const exactMapEntries = [];
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
const result = resolve(rec.provider, rec.raw_model_id);
|
|
// Strip provenance from emitted result
|
|
const clean = { ...result };
|
|
delete clean._provenance;
|
|
const provNote = result._provenance
|
|
? (() => {
|
|
const p = result._provenance;
|
|
if (p.source !== "exact") return `// source: ${p.source}`;
|
|
return [
|
|
`// provenance: exact(${p.exact_key})`,
|
|
`// registry_label: ${p.registry_label}`,
|
|
`// supported_efforts: ${p.supported_efforts}`,
|
|
`// databricks_v2_wire_route: ${p.databricks_v2_wire_route}`,
|
|
`// thinking_mode: ${p.thinking_mode}`,
|
|
`// normalization_policy: ${p.normalization_policy}`,
|
|
`// default_effort: ${p.default_effort}`,
|
|
].join("\n ");
|
|
})()
|
|
: "";
|
|
exactMapEntries.push({ rec, clean, provNote });
|
|
}
|
|
|
|
// Build provider fallback static arrays
|
|
function emitRustFallbackFn(provider, state, rec) {
|
|
const clean = { ...rec };
|
|
delete clean._provenance;
|
|
const stateId = state === "blank" ? "Blank" : "ConcreteUnknown";
|
|
return emitRustCapabilityResult(clean, " ");
|
|
}
|
|
|
|
// Collect all providers for fallback fns
|
|
const providerFallbackKeys = Object.keys(manifest.provider_fallbacks).filter(
|
|
(k) => k !== "_default",
|
|
);
|
|
|
|
const rustContent = `// @generated — do not edit by hand.
|
|
// Regenerate with: node scripts/generate-model-capabilities.mjs
|
|
// Source: scripts/model-capabilities.json
|
|
|
|
//! Generated model-capability lookup tables.
|
|
//!
|
|
//! Resolution is a total function \`resolve(provider, raw_model_id) → CapabilityResult\`.
|
|
//! Three ordered steps (plan v4 resolver contract):
|
|
//! 1. Provider-qualified raw exact lookup (before any prefix stripping).
|
|
//! 2. Provider-scoped ordered family rules on the normalized alias.
|
|
//! 3. Per-axis provider fallback (blank vs concrete-unknown).
|
|
//!
|
|
//! The manifest (scripts/model-capabilities.json) is the single source of truth.
|
|
//! This file is regenerated by scripts/generate-model-capabilities.mjs.
|
|
//! CI verifies that generated files match the manifest (regenerate-then-diff).
|
|
//!
|
|
//! Boundaries this file does NOT own (plan v4 §Boundaries):
|
|
//! - Transport/endpoint selection for pure OpenAI, legacy Databricks, OpenRouter:
|
|
//! OpenAiApi / openai_request() remain authoritative.
|
|
//! - Final display labels: resolveModelLabel() three-tier precedence is authoritative.
|
|
//! \`registry_label\` here feeds only the static registry tier.
|
|
//! - llm.rs replacement scope: ONLY \`databricks_v2_route_for_model\`.
|
|
|
|
use crate::config::ThinkingEffort;
|
|
use std::borrow::Cow;
|
|
|
|
/// Which Databricks v2 gateway wire path to use for a model.
|
|
/// Scoped to DBv2 only — other providers use \`NotApplicable\`.
|
|
/// Transport for pure OpenAI, legacy Databricks, and OpenRouter is selected
|
|
/// by OpenAiApi / openai_request() at runtime, not by this manifest.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DatabricksV2Route {
|
|
/// /ai-gateway/openai/v1/responses
|
|
OpenAiResponses,
|
|
/// /ai-gateway/anthropic/v1/messages
|
|
AnthropicMessages,
|
|
/// /ai-gateway/mlflow/v1/chat/completions
|
|
MlflowChatCompletions,
|
|
/// DBv2 blank model — route not yet determinable.
|
|
RouteUnknown,
|
|
/// Not a DBv2 provider — transport is selected by OpenAiApi/openai_request().
|
|
NotApplicable,
|
|
}
|
|
|
|
/// Anthropic thinking API shape for this model.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ThinkingMode {
|
|
/// thinking:{type:"enabled", budget_tokens} — claude-3*, claude-opus-4-5
|
|
ManualBudget,
|
|
/// thinking:{type:"adaptive"} + output_config:{effort} — opus-4-6+, sonnet-4-6+, etc.
|
|
Adaptive,
|
|
/// Unknown Anthropic model — omit thinking fields rather than guess request shape.
|
|
OmitFields,
|
|
/// Non-Anthropic-routed model — thinking fields are not applicable.
|
|
None,
|
|
/// Provider does not use Anthropic thinking API at all.
|
|
NotApplicable,
|
|
}
|
|
|
|
/// How to normalize effort values before sending to the provider.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NormalizationPolicy {
|
|
/// Pass effort through unchanged.
|
|
None,
|
|
/// Apply per-family effort table; none↔minimal peer fallback + upward tie preference.
|
|
OpenAiStandard,
|
|
/// Unknown OpenAI model: clamp max → xhigh, pass all others unchanged.
|
|
OpenAiClampMaxToXHigh,
|
|
}
|
|
|
|
/// Complete resolved capability record for a (provider, raw_model_id) pair.
|
|
/// Every axis is populated — runtime consumers do not compose fields.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct CapabilityResult {
|
|
/// Optional static display label. Feeds resolveModelLabel()'s registry tier only.
|
|
/// The dynamic three-tier precedence (discovered_name > registry_label > raw_id)
|
|
/// lives in formatAgentModelLabel / resolveModelLabel — NOT in this struct.
|
|
pub registry_label: Option<&'static str>,
|
|
/// Anthropic thinking API shape for this model.
|
|
pub thinking_mode: ThinkingMode,
|
|
/// Valid effort values for the model's effort dropdown (UI).
|
|
pub supported_efforts: Cow<'static, [ThinkingEffort]>,
|
|
/// Semantic default, or None when "Inherit" is the natural default (manual-budget Anthropic).
|
|
pub default_effort: Option<ThinkingEffort>,
|
|
/// DBv2 wire route; NotApplicable for non-DBv2 providers.
|
|
pub databricks_v2_wire_route: DatabricksV2Route,
|
|
/// Effort normalization policy before sending to provider.
|
|
pub normalization_policy: NormalizationPolicy,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exact records — provider-qualified (provider, raw_model_id), pre-prefix-stripping
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Returns the exact capability record for a provider-qualified raw model ID,
|
|
/// if one exists in the manifest. This is checked BEFORE any prefix stripping.
|
|
/// Matching is case-insensitive — both inputs are lowercased before comparison.
|
|
pub fn lookup_exact(provider: &str, raw_model_id: &str) -> Option<CapabilityResult> {
|
|
let prov_lc = provider.to_lowercase();
|
|
let id_lc = raw_model_id.to_lowercase();
|
|
match (prov_lc.as_str(), id_lc.as_str()) {
|
|
${exactMapEntries
|
|
.map(({ rec, clean, provNote }) => {
|
|
return ` ("${rec.provider.toLowerCase()}", "${rec.raw_model_id.toLowerCase()}") => {
|
|
${provNote}
|
|
Some(
|
|
${emitRustCapabilityResult(clean, " ")}
|
|
)
|
|
}`;
|
|
})
|
|
.join("\n")}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Family rule resolution — normalized alias after prefix stripping
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Strip any catalog-naming prefix to get the normalized model alias for family matching.
|
|
/// Finds the first boundary-aligned occurrence of a known family token and returns from there.
|
|
/// Boundary-aligned: the token must start at position 0 or be preceded by a non-alphanumeric char.
|
|
/// This prevents "customgpt-5-5-endpoint" from stripping to "gpt-5-5-endpoint" via "gpt-" inside
|
|
/// the "customgpt-" prefix.
|
|
///
|
|
/// Examples:
|
|
/// "goose-claude-fable-5" → "claude-fable-5" (boundary: preceded by "-")
|
|
/// "databricks-gpt-5.5" → "gpt-5.5" (boundary: preceded by "-")
|
|
/// "team-x-claude-opus-4-7" → "claude-opus-4-7" (boundary: preceded by "-")
|
|
/// "claude-opus-4-7" → "claude-opus-4-7" (no prefix, already boundary)
|
|
/// "customgpt-5-5-ep" → "customgpt-5-5-ep" (no boundary match for "gpt-")
|
|
/// "llama-3" → "llama-3" (no family token)
|
|
pub fn strip_catalog_prefix(model: &str) -> &str {
|
|
const FAMILY_TOKENS: &[&str] = &[${manifest.family_tokens.map((t) => `"${t}"`).join(", ")}];
|
|
let lower = model.to_ascii_lowercase();
|
|
let mut first_idx: Option<usize> = None;
|
|
for tok in FAMILY_TOKENS {
|
|
let tok_bytes = tok.as_bytes();
|
|
let lower_bytes = lower.as_bytes();
|
|
let mut start = 0usize;
|
|
loop {
|
|
match lower_bytes[start..].windows(tok_bytes.len()).position(|w| w == tok_bytes) {
|
|
None => break,
|
|
Some(rel) => {
|
|
let idx = start + rel;
|
|
// Boundary check: position 0 or preceded by a non-alphanumeric byte
|
|
let at_boundary = idx == 0 || {
|
|
let prev = lower_bytes[idx - 1];
|
|
!prev.is_ascii_alphanumeric()
|
|
};
|
|
if at_boundary {
|
|
first_idx = Some(first_idx.map_or(idx, |f| f.min(idx)));
|
|
break;
|
|
}
|
|
start = idx + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match first_idx {
|
|
Some(idx) => &model[idx..],
|
|
None => model,
|
|
}
|
|
}
|
|
|
|
${emitRustFamilyResolverFn()}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider fallbacks — blank vs concrete-unknown, per provider
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Returns the fallback capability record for the given provider and model state.
|
|
/// \`is_blank\` is true when the model string is empty/whitespace; false when it is
|
|
/// a nonblank but unmatched concrete ID.
|
|
pub fn provider_fallback(provider: &str, is_blank: bool) -> CapabilityResult {
|
|
match (provider, is_blank) {
|
|
${providerFallbackKeys
|
|
.map((provider) => {
|
|
const fb = manifest.provider_fallbacks[provider];
|
|
const blankClean = { ...fb.blank };
|
|
delete blankClean._provenance;
|
|
const concClean = { ...fb.concrete_unknown };
|
|
delete concClean._provenance;
|
|
return ` ("${provider}", true) => {
|
|
${emitRustCapabilityResult({ ...blankClean, registry_label: null }, " ")}
|
|
}
|
|
("${provider}", false) => {
|
|
${emitRustCapabilityResult({ ...concClean, registry_label: null }, " ")}
|
|
}`;
|
|
})
|
|
.join("\n")}
|
|
// Default fallback for unknown/empty providers
|
|
(_, true) => {
|
|
${emitRustCapabilityResult(
|
|
{
|
|
...manifest.provider_fallbacks["_default"].blank,
|
|
registry_label: null,
|
|
},
|
|
" ",
|
|
)}
|
|
}
|
|
(_, false) => {
|
|
${emitRustCapabilityResult(
|
|
{
|
|
...manifest.provider_fallbacks["_default"].concrete_unknown,
|
|
registry_label: null,
|
|
},
|
|
" ",
|
|
)}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Top-level resolve — total function, always returns a complete result
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Resolve (provider, raw_model_id) → CapabilityResult.
|
|
///
|
|
/// This is the single entry point. Result is complete — every axis is populated.
|
|
/// Consumers never compose fields from multiple tiers at runtime.
|
|
///
|
|
/// Resolution order (plan v4 resolver contract):
|
|
/// 1. provider-qualified raw exact lookup (before any prefix stripping)
|
|
/// 2. provider-scoped family rules on normalized alias
|
|
/// 3. per-axis provider fallback (blank vs concrete-unknown)
|
|
pub fn resolve_model_capabilities(provider: &str, raw_model_id: &str) -> CapabilityResult {
|
|
// Step 1: raw exact lookup
|
|
if let Some(exact) = lookup_exact(provider, raw_model_id) {
|
|
return exact;
|
|
}
|
|
|
|
// Step 2: family rules on normalized alias
|
|
let normalized = strip_catalog_prefix(raw_model_id);
|
|
if let Some(family) = lookup_by_family_rules(provider, normalized) {
|
|
return family;
|
|
}
|
|
|
|
// Step 3: provider fallback
|
|
let is_blank = raw_model_id.trim().is_empty();
|
|
let mut fallback = provider_fallback(provider, is_blank);
|
|
fallback.registry_label = None;
|
|
fallback
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generated constants (fold-in from #3603 branch)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Valid thinking-effort values accepted by buzz-agent.
|
|
/// Mirrors parse_thinking_effort in config.rs.
|
|
pub const THINKING_EFFORT_VALUES: &[&str] = &[${VALID_EFFORTS.map((e) => `"${e}"`).join(", ")}];
|
|
|
|
/// Databricks v2 known model IDs. Mirrors goose DATABRICKS_V2_KNOWN_MODELS.
|
|
/// Single source of truth inside buzz; generated from manifest databricks_v2_known_models section.
|
|
pub const DATABRICKS_V2_KNOWN_MODELS: &[&str] = &[
|
|
${manifest.databricks_v2_known_models
|
|
.map((id) => ` "${id}",`)
|
|
.join("\n")}
|
|
];
|
|
|
|
/// Databricks endpoint-ID to display-name registry. Generated from manifest registry_labels section.
|
|
/// Feeds the static registry tier of resolveModelLabel(). Final display label is determined
|
|
/// by the three-tier precedence in resolveModelLabel() (discovered_name > registry_label > raw_id).
|
|
pub const DATABRICKS_MODEL_NAMES: &[(&str, &str)] = &[
|
|
${registryLabelsArr
|
|
.map(({ id, label }) => ` ("${id}", "${label}"),`)
|
|
.join("\n")}
|
|
];
|
|
`;
|
|
|
|
function emitRustFamilyResolverFn() {
|
|
// Generate match arms for each family rule, sorted by priority desc
|
|
const sorted = [...manifest.family_rules].sort((a, b) => b.match_priority - a.match_priority);
|
|
|
|
// Group by provider for the generated fn
|
|
const allProviders = [
|
|
...new Set(sorted.flatMap((r) => r.providers)),
|
|
];
|
|
|
|
// We emit a single fn that takes (provider: &str, normalized: &str)
|
|
// and returns Option<CapabilityResult>. We use if-else chains.
|
|
|
|
const arms = [];
|
|
for (const rule of sorted) {
|
|
for (const provider of rule.providers) {
|
|
const clean = { ...rule };
|
|
delete clean._provenance;
|
|
// databricks_v2_wire_route is only meaningful for databricks_v2; all other providers get not-applicable
|
|
const wireRoute = provider === "databricks_v2"
|
|
? rule.databricks_v2_wire_route
|
|
: "not-applicable";
|
|
const matchExpr = buildRustMatchExpr(rule, provider);
|
|
arms.push(
|
|
` // rule: ${rule.id}, provider: ${provider}, priority: ${rule.match_priority}\n if provider == "${provider}" && (${matchExpr}) {\n return Some(\n${emitRustCapabilityResult(
|
|
{
|
|
registry_label: rule.registry_label ?? null,
|
|
thinking_mode: rule.thinking_mode,
|
|
supported_efforts: rule.supported_efforts,
|
|
default_effort: rule.default_effort,
|
|
databricks_v2_wire_route: wireRoute,
|
|
normalization_policy: rule.normalization_policy,
|
|
},
|
|
" ",
|
|
)}\n );\n }`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return `/// Resolve capability by family rules on the normalized (prefix-stripped) alias.
|
|
/// Returns None if no rule matches (caller falls through to provider_fallback).
|
|
///
|
|
/// Rules are ordered by match_priority descending.
|
|
/// Generated from manifest family_rules.
|
|
pub fn lookup_by_family_rules(provider: &str, normalized: &str) -> Option<CapabilityResult> {
|
|
let lower = normalized.to_ascii_lowercase();
|
|
let lower = lower.as_str();
|
|
#[allow(clippy::nonminimal_bool)]
|
|
${arms.join("\n")}
|
|
None
|
|
}`;
|
|
}
|
|
|
|
function buildRustMatchExpr(rule, provider) {
|
|
const allTokens = [rule.match_value, ...(rule.match_aliases ?? [])];
|
|
switch (rule.match_kind) {
|
|
case "exact":
|
|
return allTokens.map((t) => `lower == "${t.toLowerCase()}"`).join(" || ");
|
|
case "prefix":
|
|
return allTokens.map((t) => `lower.starts_with("${t.toLowerCase()}")`).join(" || ");
|
|
case "gpt5-token":
|
|
return allTokens
|
|
.map((t) => `gpt5_token_matches_rs(lower, "${t.toLowerCase()}")`)
|
|
.join(" || ");
|
|
case "gpt5-base":
|
|
return allTokens.map((t) => `gpt5_base_matches_rs(lower, "${t.toLowerCase()}")`).join(" || ");
|
|
case "segment": {
|
|
// Use a contains approach: split on non-alphanumeric, check any segment equals token
|
|
return allTokens
|
|
.map((t) => `lower.split(|c: char| !c.is_ascii_alphanumeric()).any(|s| s == "${t.toLowerCase()}")`)
|
|
.join(" || ");
|
|
}
|
|
case "segment-prefix": {
|
|
return allTokens
|
|
.map(
|
|
(t) =>
|
|
`lower.split(|c: char| !c.is_ascii_alphanumeric()).any(|s| s.starts_with("${t.toLowerCase()}"))`,
|
|
)
|
|
.join(" || ");
|
|
}
|
|
case "gpt-version-segment":
|
|
return allTokens.map((t) => `gpt_version_segment_matches_rs(lower, "${t.toLowerCase()}")`).join(" || ");
|
|
default:
|
|
throw new Error(`unknown match_kind: ${rule.match_kind}`);
|
|
}
|
|
}
|
|
|
|
// Append gpt5 helper fns that the generated code calls
|
|
const rustGpt5Helpers = `
|
|
// ---------------------------------------------------------------------------
|
|
// gpt5 boundary-aware token helpers (used by generated family resolver)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Returns true if \`model\` contains \`token\` at left+right word boundaries.
|
|
/// Left boundary: start-of-string or preceded by '-' or '.'.
|
|
/// Right boundary: end-of-string or followed by '-'.
|
|
/// Does not match if followed immediately by a digit or letter.
|
|
fn gpt5_token_matches_rs(model: &str, token: &str) -> bool {
|
|
let lower = model;
|
|
let tok_lower = token;
|
|
let mut start = 0;
|
|
loop {
|
|
match lower[start..].find(tok_lower) {
|
|
None => return false,
|
|
Some(rel_idx) => {
|
|
let abs_idx = start + rel_idx;
|
|
let after_idx = abs_idx + tok_lower.len();
|
|
// Left-boundary check: must start at string start or after '-' or '.'.
|
|
let left_ok = abs_idx == 0 || {
|
|
let prev = lower.as_bytes()[abs_idx - 1];
|
|
prev == b'-' || prev == b'.'
|
|
};
|
|
if !left_ok {
|
|
start = after_idx;
|
|
continue;
|
|
}
|
|
let after_char = lower[after_idx..].chars().next();
|
|
match after_char {
|
|
None | Some('-') => return true,
|
|
_ => start = after_idx,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Like gpt5_token_matches_rs but also rejects short -<1-3 digit> suffixes.
|
|
fn gpt5_base_matches_rs(model: &str, token: &str) -> bool {
|
|
let lower = model;
|
|
let tok_lower = token;
|
|
let mut start = 0;
|
|
loop {
|
|
match lower[start..].find(tok_lower) {
|
|
None => return false,
|
|
Some(rel_idx) => {
|
|
let abs_idx = start + rel_idx;
|
|
let after_idx = abs_idx + tok_lower.len();
|
|
// Left-boundary check: must start at string start or after '-' or '.'.
|
|
let left_ok = abs_idx == 0 || {
|
|
let prev = lower.as_bytes()[abs_idx - 1];
|
|
prev == b'-' || prev == b'.'
|
|
};
|
|
if !left_ok {
|
|
start = after_idx;
|
|
continue;
|
|
}
|
|
let suffix = &lower[after_idx..];
|
|
if suffix.is_empty() {
|
|
return true;
|
|
}
|
|
if !suffix.starts_with('-') {
|
|
start = after_idx;
|
|
continue;
|
|
}
|
|
let dash_rest = &suffix[1..];
|
|
// Reject -<1-3 digits> followed by non-alphanumeric or end (mirrors TS /^\\d{1,3}(?:[^a-z\\d]|$)/i).
|
|
let first_non_digit = dash_rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(dash_rest.len());
|
|
let is_short_version = (1..=3).contains(&first_non_digit)
|
|
&& (first_non_digit == dash_rest.len()
|
|
|| !dash_rest.as_bytes()[first_non_digit].is_ascii_alphanumeric());
|
|
if is_short_version {
|
|
start = after_idx;
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// gpt-version-segment helper (used by generated family resolver)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// gpt-version-segment match: token is an exact segment AND (token itself starts with a digit,
|
|
/// OR the next segment after it starts with a digit). Prevents "gpt-neox-20b" from matching
|
|
/// "gpt" because "neox" starts with a letter, while "gpt-5.5" matches because next seg "5" is digit.
|
|
fn gpt_version_segment_matches_rs(model: &str, token: &str) -> bool {
|
|
let segs: Vec<&str> = model.split(|c: char| !c.is_ascii_alphanumeric()).collect();
|
|
for (i, seg) in segs.iter().enumerate() {
|
|
if *seg == token {
|
|
// Dashless numeric form (e.g. "gpt5"): token length > 3 or starts with digit
|
|
if token.len() > 3 || token.as_bytes().first().is_some_and(|b| b.is_ascii_digit()) {
|
|
return true;
|
|
}
|
|
// For short alpha tokens like "gpt": require the next segment to start with a digit
|
|
if let Some(next_seg) = segs.get(i + 1) {
|
|
if next_seg.as_bytes().first().is_some_and(|b| b.is_ascii_digit()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
#[path = "generated_model_capabilities_tests.rs"]
|
|
mod tests;
|
|
`;
|
|
|
|
const finalRustContent = rustfmt(rustContent + rustGpt5Helpers);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TypeScript code generation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function tsEffortList(efforts) {
|
|
return `[${efforts.map((e) => `"${e}"`).join(", ")}] as const`;
|
|
}
|
|
|
|
function tsEffortOrNull(e) {
|
|
if (e === null || e === undefined) return "null";
|
|
return `"${e}"`;
|
|
}
|
|
|
|
function tsDbv2Route(route) {
|
|
return `"${route}"`;
|
|
}
|
|
|
|
function tsThinkingMode(mode) {
|
|
return `"${mode}"`;
|
|
}
|
|
|
|
function tsNormPolicy(policy) {
|
|
return `"${policy}"`;
|
|
}
|
|
|
|
function emitTsCapabilityResult(r, indent = " ") {
|
|
const i = indent;
|
|
return [
|
|
`{`,
|
|
`${i} registryLabel: ${r.registry_label === null ? "null" : `"${r.registry_label}"`},`,
|
|
`${i} thinkingMode: ${tsThinkingMode(r.thinking_mode)},`,
|
|
`${i} supportedEfforts: ${tsEffortList(r.supported_efforts)},`,
|
|
`${i} defaultEffort: ${tsEffortOrNull(r.default_effort)},`,
|
|
`${i} databricksV2WireRoute: ${tsDbv2Route(r.databricks_v2_wire_route)},`,
|
|
`${i} normalizationPolicy: ${tsNormPolicy(r.normalization_policy)},`,
|
|
`${i}}`,
|
|
].join(`\n${i}`);
|
|
}
|
|
|
|
// Build TS exact records map
|
|
const tsExactEntries = [];
|
|
for (const rec of manifest.exact_records ?? []) {
|
|
const result = resolve(rec.provider, rec.raw_model_id);
|
|
const clean = { ...result };
|
|
delete clean._provenance;
|
|
tsExactEntries.push({ rec, clean });
|
|
}
|
|
|
|
// Build TS family rule resolution (inlined ordered if-chain for readability)
|
|
function emitTsFamilyResolver() {
|
|
const sorted = [...manifest.family_rules].sort((a, b) => b.match_priority - a.match_priority);
|
|
const arms = [];
|
|
for (const rule of sorted) {
|
|
for (const provider of rule.providers) {
|
|
// databricks_v2_wire_route is only meaningful for databricks_v2; all other providers get not-applicable
|
|
const wireRoute = provider === "databricks_v2"
|
|
? rule.databricks_v2_wire_route
|
|
: "not-applicable";
|
|
const clean = {
|
|
registry_label: rule.registry_label ?? null,
|
|
thinking_mode: rule.thinking_mode,
|
|
supported_efforts: rule.supported_efforts,
|
|
default_effort: rule.default_effort,
|
|
databricks_v2_wire_route: wireRoute,
|
|
normalization_policy: rule.normalization_policy,
|
|
};
|
|
const matchExpr = buildTsMatchExpr(rule, provider);
|
|
arms.push(
|
|
` // rule: ${rule.id}, provider: ${provider}, priority: ${rule.match_priority}\n if (provider === "${provider}" && (${matchExpr})) {\n return ${emitTsCapabilityResult(clean, " ")};\n }`,
|
|
);
|
|
}
|
|
}
|
|
return arms.join("\n");
|
|
}
|
|
|
|
function buildTsMatchExpr(rule, provider) {
|
|
const allTokens = [rule.match_value, ...(rule.match_aliases ?? [])];
|
|
switch (rule.match_kind) {
|
|
case "exact":
|
|
return allTokens.map((t) => `lower === "${t.toLowerCase()}"`).join(" || ");
|
|
case "prefix":
|
|
return allTokens.map((t) => `lower.startsWith("${t.toLowerCase()}")`).join(" || ");
|
|
case "gpt5-token":
|
|
return allTokens
|
|
.map((t) => `gpt5TokenMatchesGenerated(lower, "${t.toLowerCase()}")`)
|
|
.join(" || ");
|
|
case "gpt5-base":
|
|
return allTokens
|
|
.map((t) => `gpt5BaseMatchesGenerated(lower, "${t.toLowerCase()}")`)
|
|
.join(" || ");
|
|
case "segment":
|
|
return allTokens
|
|
.map(
|
|
(t) =>
|
|
`lower.split(/[^a-z0-9]+/).includes("${t.toLowerCase()}")`,
|
|
)
|
|
.join(" || ");
|
|
case "segment-prefix":
|
|
return allTokens
|
|
.map(
|
|
(t) =>
|
|
`lower.split(/[^a-z0-9]+/).some(s => s.startsWith("${t.toLowerCase()}"))`,
|
|
)
|
|
.join(" || ");
|
|
case "gpt-version-segment":
|
|
return allTokens.map((t) => `gptVersionSegmentMatchesGenerated(lower, "${t.toLowerCase()}")`).join(" || ");
|
|
default:
|
|
throw new Error(`unknown match_kind: ${rule.match_kind}`);
|
|
}
|
|
}
|
|
|
|
const tsProviderFallbacks = providerFallbackKeys
|
|
.map((provider) => {
|
|
const fb = manifest.provider_fallbacks[provider];
|
|
const blankClean = { ...fb.blank, registry_label: null };
|
|
const concClean = { ...fb.concrete_unknown, registry_label: null };
|
|
delete blankClean._provenance;
|
|
delete concClean._provenance;
|
|
return ` ["${provider}", {
|
|
blank: ${emitTsCapabilityResult(blankClean, " ")},
|
|
concreteUnknown: ${emitTsCapabilityResult(concClean, " ")},
|
|
}],`;
|
|
})
|
|
.join("\n");
|
|
|
|
const defaultFbBlank = { ...manifest.provider_fallbacks["_default"].blank, registry_label: null };
|
|
const defaultFbConc = {
|
|
...manifest.provider_fallbacks["_default"].concrete_unknown,
|
|
registry_label: null,
|
|
};
|
|
delete defaultFbBlank._provenance;
|
|
delete defaultFbConc._provenance;
|
|
|
|
const tsContent = `// biome-ignore-all format: generated — do not edit by hand.
|
|
// Regenerate with: node scripts/generate-model-capabilities.mjs
|
|
// Source: scripts/model-capabilities.json
|
|
//
|
|
// Resolver: provider+rawModelId → exact lookup → family rules → provider fallback (plan v4).
|
|
// Not owned here: OpenAI/legacy Databricks/OpenRouter transport; final labels (resolveModelLabel() authoritative).
|
|
|
|
/** Valid thinking-effort values accepted by buzz-agent (mirrors parse_thinking_effort in config.rs). */
|
|
export const THINKING_EFFORT_VALUES = [${VALID_EFFORTS.map((e) => `"${e}"`).join(", ")}] as const;
|
|
export type ThinkingEffortValue = (typeof THINKING_EFFORT_VALUES)[number];
|
|
|
|
/** Databricks v2 wire route. NotApplicable for non-DBv2 providers. */
|
|
export type DatabricksV2WireRoute =
|
|
| "openai-responses"
|
|
| "anthropic-messages"
|
|
| "mlflow-chat"
|
|
| "route-unknown"
|
|
| "not-applicable";
|
|
|
|
/** Anthropic thinking API shape for this model. */
|
|
export type ThinkingMode =
|
|
| "manual-budget"
|
|
| "adaptive"
|
|
| "omit-fields"
|
|
| "none"
|
|
| "not-applicable";
|
|
|
|
/** How to normalize effort values before sending to the provider. */
|
|
export type NormalizationPolicy = "none" | "openai-standard" | "openai-clamp-max-to-xhigh";
|
|
|
|
/** Complete resolved capability record for a (provider, rawModelId) pair. Every axis populated. */
|
|
export type CapabilityResult = {
|
|
/** Optional static display label. Feeds resolveModelLabel()'s registry tier only. */
|
|
readonly registryLabel: string | null;
|
|
readonly thinkingMode: ThinkingMode;
|
|
readonly supportedEfforts: ReadonlyArray<ThinkingEffortValue>;
|
|
readonly defaultEffort: ThinkingEffortValue | null;
|
|
readonly databricksV2WireRoute: DatabricksV2WireRoute;
|
|
readonly normalizationPolicy: NormalizationPolicy;
|
|
};
|
|
|
|
/** Valid thinking-effort values accepted by buzz-agent. */
|
|
export const BUZZ_AGENT_THINKING_EFFORT_VALUES = THINKING_EFFORT_VALUES;
|
|
|
|
/** Databricks v2 known model IDs. Mirrors goose DATABRICKS_V2_KNOWN_MODELS. */
|
|
export const DATABRICKS_V2_KNOWN_MODELS = [
|
|
${manifest.databricks_v2_known_models
|
|
.map((id) => ` "${id}",`)
|
|
.join("\n")}
|
|
] as const;
|
|
|
|
/** Databricks endpoint-ID to display-name registry. Generated from manifest registry_labels section.
|
|
* Feeds the static registry tier of resolveModelLabel(). */
|
|
export const DATABRICKS_MODEL_NAMES: Map<string, string> = new Map([
|
|
${registryLabelsArr
|
|
.map(({ id, label }) => ` ["${id}", "${label}"],`)
|
|
.join("\n")}
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// gpt5 boundary-aware token helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function hasLeftBoundaryGenerated(m: string, idx: number): boolean {
|
|
if (idx === 0) return true;
|
|
const prev = m[idx - 1];
|
|
return prev === "-" || prev === ".";
|
|
}
|
|
|
|
function gpt5TokenMatchesGenerated(m: string, token: string): boolean {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = m.indexOf(token, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + token.length;
|
|
if (!hasLeftBoundaryGenerated(m, idx)) { start = afterIdx; continue; }
|
|
const afterChar = afterIdx < m.length ? m[afterIdx] : "";
|
|
if (afterChar === "" || afterChar === "-") return true;
|
|
start = afterIdx;
|
|
}
|
|
}
|
|
|
|
function gpt5BaseMatchesGenerated(m: string, token: string): boolean {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = m.indexOf(token, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + token.length;
|
|
if (!hasLeftBoundaryGenerated(m, idx)) { start = afterIdx; continue; }
|
|
const suffix = m.slice(afterIdx);
|
|
if (suffix === "") return true;
|
|
if (!suffix.startsWith("-")) { start = afterIdx; continue; }
|
|
const dashRest = suffix.slice(1);
|
|
if (/^\\d{1,3}(?:[^a-z\\d]|$)/i.test(dashRest)) { start = afterIdx; continue; }
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* gpt-version-segment match: token is an exact segment AND (token itself starts with a digit,
|
|
* OR the next segment after it starts with a digit). Prevents "gpt-neox-20b" from matching
|
|
* "gpt" because "neox" starts with a letter, while "gpt-5.5" matches because next seg "5" is digit.
|
|
*/
|
|
function gptVersionSegmentMatchesGenerated(m: string, token: string): boolean {
|
|
const segs = m.split(/[^a-z0-9]+/);
|
|
for (let i = 0; i < segs.length; i++) {
|
|
if (segs[i] === token) {
|
|
// Dashless numeric form (e.g. "gpt5") or token itself starts with digit
|
|
if (token.length > 3 || /^\\d/.test(token)) return true;
|
|
// For "gpt": require the next segment to start with a digit
|
|
const nextSeg = segs[i + 1];
|
|
if (nextSeg !== undefined && /^\\d/.test(nextSeg)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exact records — provider-qualified, pre-prefix-stripping
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const EXACT_RECORDS = new Map<string, CapabilityResult>([
|
|
${tsExactEntries
|
|
.map(({ rec, clean }) => {
|
|
// Keys are lowercased at build time; resolveModelCapabilities lowercases at lookup time.
|
|
return ` ["${rec.provider.toLowerCase()}::${rec.raw_model_id.toLowerCase()}", ${emitTsCapabilityResult(clean, " ")}],`;
|
|
})
|
|
.join("\n")}
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider fallbacks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PROVIDER_FALLBACKS = new Map<string, { blank: CapabilityResult; concreteUnknown: CapabilityResult }>([
|
|
${tsProviderFallbacks}
|
|
]);
|
|
|
|
const DEFAULT_FALLBACK = {
|
|
blank: ${emitTsCapabilityResult(defaultFbBlank, " ")},
|
|
concreteUnknown: ${emitTsCapabilityResult(defaultFbConc, " ")},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Strip catalog prefix — boundary-aware, finds first boundary-aligned family token
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function stripCatalogPrefix(model: string): string {
|
|
const FAMILY_TOKENS = [${manifest.family_tokens.map((t) => `"${t}"`).join(", ")}] as const;
|
|
const lower = model.toLowerCase();
|
|
let firstIdx = Infinity;
|
|
for (const tok of FAMILY_TOKENS) {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = lower.indexOf(tok, start);
|
|
if (idx === -1) break;
|
|
// Boundary check: position 0 or preceded by a non-alphanumeric character
|
|
if (idx === 0 || !/[a-z0-9]/.test(lower[idx - 1])) {
|
|
if (idx < firstIdx) firstIdx = idx;
|
|
break;
|
|
}
|
|
start = idx + 1;
|
|
}
|
|
}
|
|
return firstIdx === Infinity ? model : model.slice(firstIdx);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Family rule resolver (generated ordered if-chain)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function lookupByFamilyRules(provider: string, normalized: string): CapabilityResult | null {
|
|
const lower = normalized.toLowerCase();
|
|
${emitTsFamilyResolver()}
|
|
return null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Top-level resolve — total function, always returns a complete result
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve (provider, rawModelId) → CapabilityResult.
|
|
*
|
|
* Total function — always returns a complete result. Consumers never compose
|
|
* fields from multiple tiers at runtime.
|
|
*
|
|
* Resolution order (plan v4):
|
|
* 1. Provider-qualified raw exact lookup (before prefix stripping)
|
|
* 2. Provider-scoped family rules on normalized alias
|
|
* 3. Per-axis provider fallback (blank vs concrete-unknown)
|
|
*/
|
|
export function resolveModelCapabilities(
|
|
provider: string,
|
|
rawModelId: string,
|
|
): CapabilityResult {
|
|
// Step 1: raw exact lookup (case-insensitive — keys lowercased at build time)
|
|
const exactKey = \`\${provider.toLowerCase()}::\${rawModelId.toLowerCase()}\`;
|
|
const exact = EXACT_RECORDS.get(exactKey);
|
|
if (exact) return exact;
|
|
|
|
// Step 2: family rules on normalized alias
|
|
const normalized = stripCatalogPrefix(rawModelId);
|
|
const family = lookupByFamilyRules(provider, normalized);
|
|
if (family) return family;
|
|
|
|
// Step 3: provider fallback
|
|
const isBlank = rawModelId.trim() === "";
|
|
const fb = PROVIDER_FALLBACKS.get(provider) ?? DEFAULT_FALLBACK;
|
|
return isBlank ? { ...fb.blank, registryLabel: null } : { ...fb.concreteUnknown, registryLabel: null };
|
|
}
|
|
`;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Write or check files
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const outputs = [
|
|
{
|
|
path: outputDirOverride
|
|
? join(outputDirOverride, "generated_model_capabilities.rs")
|
|
: join(repoRoot, "crates", "buzz-agent", "src", "generated_model_capabilities.rs"),
|
|
content: finalRustContent,
|
|
label: "Rust",
|
|
},
|
|
{
|
|
path: outputDirOverride
|
|
? join(outputDirOverride, "modelCapabilities.ts")
|
|
: join(
|
|
repoRoot,
|
|
"desktop",
|
|
"src",
|
|
"features",
|
|
"agents",
|
|
"ui",
|
|
"modelCapabilities.ts",
|
|
),
|
|
content: tsContent,
|
|
label: "TypeScript",
|
|
},
|
|
];
|
|
|
|
let checkFailed = false;
|
|
for (const { path, content, label } of outputs) {
|
|
if (CHECK_MODE) {
|
|
if (!existsSync(path)) {
|
|
console.error(`CHECK FAILED: ${label} file does not exist: ${path}`);
|
|
checkFailed = true;
|
|
continue;
|
|
}
|
|
const existing = readFileSync(path, "utf8");
|
|
if (existing !== content) {
|
|
console.error(`CHECK FAILED: ${label} file is stale: ${path}`);
|
|
console.error("Run: node scripts/generate-model-capabilities.mjs to regenerate.");
|
|
checkFailed = true;
|
|
} else {
|
|
console.log(`OK: ${label}`);
|
|
}
|
|
} else {
|
|
writeFileSync(path, content, "utf8");
|
|
console.log(`Wrote ${label}: ${path}`);
|
|
}
|
|
}
|
|
|
|
if (CHECK_MODE && checkFailed) {
|
|
process.exit(1);
|
|
}
|
|
if (!CHECK_MODE) {
|
|
console.log("Done. Generated 2 files.");
|
|
}
|