diff --git a/package.json b/package.json index 3a619d1ea..b9622c87b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "validate": "bun ./packages/core/script/validate.ts", "compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts", "chutes:generate": "bun ./packages/core/script/generate-chutes.ts", + "databricks:generate": "bun ./packages/core/script/generate-databricks.ts", "helicone:generate": "bun ./packages/core/script/generate-helicone.ts", "venice:generate": "bun ./packages/core/script/generate-venice.ts", "vercel:generate": "bun ./packages/core/script/generate-vercel.ts", diff --git a/packages/core/script/generate-databricks.ts b/packages/core/script/generate-databricks.ts new file mode 100644 index 000000000..8752e023b --- /dev/null +++ b/packages/core/script/generate-databricks.ts @@ -0,0 +1,287 @@ +#!/usr/bin/env bun + +/** + * Generates Databricks model TOML files from the Foundation Model API endpoint. + * + * Each Databricks endpoint exposes a model from another provider (Anthropic, + * OpenAI, Google, etc.), so the generated TOML uses [extends] to inherit + * canonical metadata from that upstream provider's TOML in models.dev. + * + * Usage: + * DATABRICKS_HOST= DATABRICKS_TOKEN= bun run databricks:generate + * bun run databricks:generate --workspace --token + * + * Flags: + * --dry-run: Preview changes without writing files + * --new-only: Only create new models, skip updating existing ones + */ + +import { z } from "zod"; +import path from "node:path"; +import { mkdir, readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; + +const args = process.argv.slice(2); +const flag = (name: string) => { + const i = args.indexOf(`--${name}`); + return i !== -1 ? args[i + 1] : undefined; +}; +const dryRun = args.includes("--dry-run"); +const newOnly = args.includes("--new-only"); + +const host = flag("workspace") ?? process.env.DATABRICKS_HOST; +const token = flag("token") ?? process.env.DATABRICKS_TOKEN; + +if (!host || !token) { + console.error( + "Usage: DATABRICKS_HOST= DATABRICKS_TOKEN= bun run databricks:generate", + ); + process.exit(1); +} + +const workspace = host.replace(/^https?:\/\//, "").replace(/\/$/, ""); +const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "providers"); +const MODELS_DIR = path.join(PROVIDERS_DIR, "databricks", "models"); + +// --------------------------------------------------------------------------- +// API schemas +// --------------------------------------------------------------------------- + +const FoundationModel = z + .object({ + ai_gateway_v2_supported: z.boolean().optional(), + api_types: z.array(z.string()).optional(), + }) + .passthrough(); + +const ServedEntity = z + .object({ + foundation_model: FoundationModel.optional(), + }) + .passthrough(); + +const Endpoint = z + .object({ + name: z.string(), + config: z + .object({ + served_entities: z.array(ServedEntity).optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + +const FoundationModelsResponse = z + .object({ + endpoints: z.array(Endpoint), + }) + .passthrough(); + +// --------------------------------------------------------------------------- +// Canonical resolution: map a Databricks endpoint name to a models.dev entry +// --------------------------------------------------------------------------- + +const PREFIX_TO_PROVIDER: [string, string][] = [ + ["claude-", "anthropic"], + ["gpt-", "openai"], + ["gemini-", "google"], + ["mistral-", "mistral"], + ["mixtral-", "mistral"], +]; + +type Resolution = + | { type: "extends"; from: string } + | { type: "inline"; content: string } + | null; + +async function resolveCanonical(endpointName: string): Promise { + const bare = endpointName.replace(/^databricks-/, ""); + + // Models in provider subdirectories (e.g. openrouter/openai/gpt-oss-*) + // can't use [extends] (schema requires provider/model format), so inline. + if (bare.startsWith("gpt-oss-")) { + const p = path.join(PROVIDERS_DIR, "openrouter", "models", "openai", `${bare}.toml`); + if (existsSync(p)) { + return { type: "inline", content: await readFile(p, "utf8") }; + } + } + + // Meta Llama: "meta-llama-3-3-70b-instruct" → "llama-3.3-70b-instruct" + if (bare.startsWith("meta-llama-") || bare.startsWith("llama-")) { + const llamaId = bare + .replace(/^meta-llama-/, "llama-") + .replace(/^(llama-\d+)-(\d+)-/, "$1.$2-"); + const p = path.join(PROVIDERS_DIR, "llama", "models", `${llamaId}.toml`); + if (existsSync(p)) return { type: "extends", from: `llama/${llamaId}` }; + } + + for (const [prefix, provider] of PREFIX_TO_PROVIDER) { + if (!bare.startsWith(prefix)) continue; + + const exact = path.join(PROVIDERS_DIR, provider, "models", `${bare}.toml`); + if (existsSync(exact)) return { type: "extends", from: `${provider}/${bare}` }; + + // Try with hyphens-as-dots in version (e.g. gpt-5-4 → gpt-5.4) + const dotted = bare.replace(/^((?:[a-z]+-)+\d+)-(\d)/, "$1.$2"); + if (dotted !== bare) { + const dottedExact = path.join(PROVIDERS_DIR, provider, "models", `${dotted}.toml`); + if (existsSync(dottedExact)) return { type: "extends", from: `${provider}/${dotted}` }; + } + + // Fuzzy: longest filename that shares a prefix with bare or its dotted form + const candidates = [bare, ...(dotted !== bare ? [dotted] : [])]; + const files: string[] = []; + try { + for await (const f of new Bun.Glob("*.toml").scan({ + cwd: path.join(PROVIDERS_DIR, provider, "models"), + })) { + files.push(f); + } + } catch { + // provider directory may not exist + } + const match = files + .map((f) => f.replace(/\.toml$/, "")) + .filter((id) => candidates.some((c) => id.startsWith(c) || c.startsWith(id))) + .sort((a, b) => b.length - a.length)[0]; + if (match) return { type: "extends", from: `${provider}/${match}` }; + } + + return null; +} + +function formatToml(resolution: Resolution, endpointName: string): string { + if (resolution?.type === "extends") { + return `[extends]\nfrom = "${resolution.from}"\n`; + } + if (resolution?.type === "inline") { + return resolution.content; + } + return `# TODO: fill in details for ${endpointName}\nname = "${endpointName}"\n`; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const IGNORE_PREFIXES = [ + "databricks-llama-", + "databricks-meta-llama-", + "databricks-qwen", + "databricks-gemma-", +]; + +async function main() { + console.log( + `${dryRun ? "[DRY RUN] " : ""}${newOnly ? "[NEW ONLY] " : ""}Fetching Databricks foundation-models...`, + ); + + const url = `https://${workspace}/api/2.0/serving-endpoints:foundation-models`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) { + console.error(`Failed to fetch API: ${res.status} ${res.statusText}`); + console.error(await res.text().catch(() => "")); + process.exit(1); + } + + const json = await res.json(); + const parsed = FoundationModelsResponse.safeParse(json); + if (!parsed.success) { + console.error("Invalid API response:", parsed.error.errors); + process.exit(1); + } + + const endpoints = parsed.data.endpoints.filter( + (e) => + !IGNORE_PREFIXES.some((p) => e.name.startsWith(p)) && + e.config?.served_entities?.some( + (se) => + se.foundation_model?.ai_gateway_v2_supported === true && + se.foundation_model?.api_types?.includes("mlflow/v1/chat/completions"), + ), + ); + + const existingFiles = new Set(); + try { + for await (const f of new Bun.Glob("*.toml").scan({ cwd: MODELS_DIR })) { + existingFiles.add(f); + } + } catch { + // directory may not exist yet + } + + console.log( + `Found ${endpoints.length} models in API, ${existingFiles.size} existing files\n`, + ); + + const apiModelIds = new Set(); + let created = 0; + let updated = 0; + let unchanged = 0; + + for (const ep of endpoints) { + const filename = `${ep.name}.toml`; + apiModelIds.add(filename); + const filePath = path.join(MODELS_DIR, filename); + + const resolution = await resolveCanonical(ep.name); + const newContent = formatToml(resolution, ep.name); + const tag = resolution?.type === "extends" ? `extends ${resolution.from}` : resolution?.type ?? "stub"; + + const existed = existsSync(filePath); + if (!existed) { + created++; + if (dryRun) { + console.log(`[DRY RUN] Would create: ${filename} → ${tag}`); + } else { + await mkdir(MODELS_DIR, { recursive: true }); + await Bun.write(filePath, newContent); + console.log(`Created: ${filename} → ${tag}`); + } + continue; + } + + if (newOnly) { + unchanged++; + continue; + } + + const existingContent = await readFile(filePath, "utf8"); + if (existingContent === newContent) { + unchanged++; + continue; + } + + updated++; + if (dryRun) { + console.log(`[DRY RUN] Would update: ${filename} → ${tag}`); + } else { + await Bun.write(filePath, newContent); + console.log(`Updated: ${filename} → ${tag}`); + } + } + + const orphaned: string[] = []; + for (const file of existingFiles) { + if (!apiModelIds.has(file)) { + orphaned.push(file); + console.log(`Warning: Orphaned file (not in API): ${file}`); + } + } + + console.log(""); + if (dryRun) { + console.log( + `Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} orphaned`, + ); + } else { + console.log( + `Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned`, + ); + } +} + +await main(); diff --git a/providers/databricks/models/databricks-claude-haiku-4-5.toml b/providers/databricks/models/databricks-claude-haiku-4-5.toml new file mode 100644 index 000000000..adde30041 --- /dev/null +++ b/providers/databricks/models/databricks-claude-haiku-4-5.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-haiku-4-5" diff --git a/providers/databricks/models/databricks-claude-opus-4-1.toml b/providers/databricks/models/databricks-claude-opus-4-1.toml new file mode 100644 index 000000000..fab528f61 --- /dev/null +++ b/providers/databricks/models/databricks-claude-opus-4-1.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-opus-4-1" diff --git a/providers/databricks/models/databricks-claude-opus-4-5.toml b/providers/databricks/models/databricks-claude-opus-4-5.toml new file mode 100644 index 000000000..bd267554f --- /dev/null +++ b/providers/databricks/models/databricks-claude-opus-4-5.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-opus-4-5" diff --git a/providers/databricks/models/databricks-claude-opus-4-6.toml b/providers/databricks/models/databricks-claude-opus-4-6.toml new file mode 100644 index 000000000..1bcb65466 --- /dev/null +++ b/providers/databricks/models/databricks-claude-opus-4-6.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-opus-4-6" diff --git a/providers/databricks/models/databricks-claude-opus-4-7.toml b/providers/databricks/models/databricks-claude-opus-4-7.toml new file mode 100644 index 000000000..f1fae3e21 --- /dev/null +++ b/providers/databricks/models/databricks-claude-opus-4-7.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-opus-4-7" diff --git a/providers/databricks/models/databricks-claude-sonnet-4-5.toml b/providers/databricks/models/databricks-claude-sonnet-4-5.toml new file mode 100644 index 000000000..024d14ea3 --- /dev/null +++ b/providers/databricks/models/databricks-claude-sonnet-4-5.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-sonnet-4-5" diff --git a/providers/databricks/models/databricks-claude-sonnet-4-6.toml b/providers/databricks/models/databricks-claude-sonnet-4-6.toml new file mode 100644 index 000000000..ed2c5807b --- /dev/null +++ b/providers/databricks/models/databricks-claude-sonnet-4-6.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-sonnet-4-6" diff --git a/providers/databricks/models/databricks-claude-sonnet-4.toml b/providers/databricks/models/databricks-claude-sonnet-4.toml new file mode 100644 index 000000000..e579bb4e5 --- /dev/null +++ b/providers/databricks/models/databricks-claude-sonnet-4.toml @@ -0,0 +1,2 @@ +[extends] +from = "anthropic/claude-sonnet-4-5-20250929" diff --git a/providers/databricks/models/databricks-gemini-2-5-flash.toml b/providers/databricks/models/databricks-gemini-2-5-flash.toml new file mode 100644 index 000000000..da23d56b3 --- /dev/null +++ b/providers/databricks/models/databricks-gemini-2-5-flash.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-2.5-flash" diff --git a/providers/databricks/models/databricks-gemini-2-5-pro.toml b/providers/databricks/models/databricks-gemini-2-5-pro.toml new file mode 100644 index 000000000..bd908b3a3 --- /dev/null +++ b/providers/databricks/models/databricks-gemini-2-5-pro.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-2.5-pro" diff --git a/providers/databricks/models/databricks-gemini-3-1-flash-lite.toml b/providers/databricks/models/databricks-gemini-3-1-flash-lite.toml new file mode 100644 index 000000000..2f00e4ee5 --- /dev/null +++ b/providers/databricks/models/databricks-gemini-3-1-flash-lite.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-3.1-flash-lite-preview" diff --git a/providers/databricks/models/databricks-gemini-3-1-pro.toml b/providers/databricks/models/databricks-gemini-3-1-pro.toml new file mode 100644 index 000000000..92b13a100 --- /dev/null +++ b/providers/databricks/models/databricks-gemini-3-1-pro.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-3.1-pro-preview-customtools" diff --git a/providers/databricks/models/databricks-gemini-3-flash.toml b/providers/databricks/models/databricks-gemini-3-flash.toml new file mode 100644 index 000000000..5ef45fd0e --- /dev/null +++ b/providers/databricks/models/databricks-gemini-3-flash.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-3-flash-preview" diff --git a/providers/databricks/models/databricks-gemini-3-pro.toml b/providers/databricks/models/databricks-gemini-3-pro.toml new file mode 100644 index 000000000..a7d2ee905 --- /dev/null +++ b/providers/databricks/models/databricks-gemini-3-pro.toml @@ -0,0 +1,2 @@ +[extends] +from = "google/gemini-3-pro-preview" diff --git a/providers/databricks/models/databricks-gpt-5-1.toml b/providers/databricks/models/databricks-gpt-5-1.toml new file mode 100644 index 000000000..ee179177b --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-1.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.1" diff --git a/providers/databricks/models/databricks-gpt-5-2.toml b/providers/databricks/models/databricks-gpt-5-2.toml new file mode 100644 index 000000000..a816a94d9 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-2.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.2" diff --git a/providers/databricks/models/databricks-gpt-5-4-mini.toml b/providers/databricks/models/databricks-gpt-5-4-mini.toml new file mode 100644 index 000000000..5620c9831 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-4-mini.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.4-mini" diff --git a/providers/databricks/models/databricks-gpt-5-4-nano.toml b/providers/databricks/models/databricks-gpt-5-4-nano.toml new file mode 100644 index 000000000..ffd93a3f5 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-4-nano.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.4-nano" diff --git a/providers/databricks/models/databricks-gpt-5-4.toml b/providers/databricks/models/databricks-gpt-5-4.toml new file mode 100644 index 000000000..c7de63530 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-4.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.4" diff --git a/providers/databricks/models/databricks-gpt-5-5.toml b/providers/databricks/models/databricks-gpt-5-5.toml new file mode 100644 index 000000000..8ab9ada46 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-5.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5.5" diff --git a/providers/databricks/models/databricks-gpt-5-mini.toml b/providers/databricks/models/databricks-gpt-5-mini.toml new file mode 100644 index 000000000..dddbc0362 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-mini.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5-mini" diff --git a/providers/databricks/models/databricks-gpt-5-nano.toml b/providers/databricks/models/databricks-gpt-5-nano.toml new file mode 100644 index 000000000..20aa087fe --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5-nano.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5-nano" diff --git a/providers/databricks/models/databricks-gpt-5.toml b/providers/databricks/models/databricks-gpt-5.toml new file mode 100644 index 000000000..62a7604b4 --- /dev/null +++ b/providers/databricks/models/databricks-gpt-5.toml @@ -0,0 +1,2 @@ +[extends] +from = "openai/gpt-5" diff --git a/providers/databricks/models/databricks-gpt-oss-120b.toml b/providers/databricks/models/databricks-gpt-oss-120b.toml new file mode 100644 index 000000000..62e3695cd --- /dev/null +++ b/providers/databricks/models/databricks-gpt-oss-120b.toml @@ -0,0 +1,22 @@ +name = "GPT OSS 120B" +family = "gpt-oss" +release_date = "2025-08-05" +last_updated = "2025-08-05" +attachment = false +reasoning = true +temperature = true +tool_call = true +structured_output = true +open_weights = true + +[cost] +input = 0.072 +output = 0.28 + +[limit] +context = 131_072 +output = 32_768 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/databricks/models/databricks-gpt-oss-20b.toml b/providers/databricks/models/databricks-gpt-oss-20b.toml new file mode 100644 index 000000000..916314f7b --- /dev/null +++ b/providers/databricks/models/databricks-gpt-oss-20b.toml @@ -0,0 +1,22 @@ +name = "GPT OSS 20B" +family = "gpt-oss" +release_date = "2025-08-05" +last_updated = "2025-08-05" +attachment = false +reasoning = true +temperature = true +tool_call = true +structured_output = true +open_weights = true + +[cost] +input = 0.05 +output = 0.20 + +[limit] +context = 131_072 +output = 32_768 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/providers/databricks/provider.toml b/providers/databricks/provider.toml new file mode 100644 index 000000000..f07eaf4ab --- /dev/null +++ b/providers/databricks/provider.toml @@ -0,0 +1,5 @@ +name = "Databricks" +npm = "@ai-sdk/openai-compatible" +api = "https://${DATABRICKS_HOST}/ai-gateway/mlflow/v1" +env = ["DATABRICKS_HOST", "DATABRICKS_TOKEN"] +doc = "https://docs.databricks.com/aws/en/machine-learning/foundation-models/"