Skip to content

feat(scripts): add models.dev export script - #1486

Merged
steebchen merged 9 commits into
mainfrom
steebchen/models-dev-export
Jan 29, 2026
Merged

feat(scripts): add models.dev export script#1486
steebchen merged 9 commits into
mainfrom
steebchen/models-dev-export

Conversation

@steebchen

@steebchen steebchen commented Jan 23, 2026

Copy link
Copy Markdown
Member

Summary

Add a script to export all models and providers from @llmgateway/models to TOML format compatible with models.dev.

Usage

npx tsx scripts/export-models-dev.ts

Output

exports/providers/
├── openai/
│   ├── provider.toml
│   └── models/
│       ├── gpt-4o.toml
│       └── ...
├── anthropic/
│   ├── provider.toml
│   └── models/
│       └── ...
└── ... (24 providers)

Features

  • Exports 222 models across 24 providers
  • Generates provider.toml with:
    • npm package (@ai-sdk/openai, etc.)
    • Environment variables
    • Documentation URL
    • API endpoint (for OpenAI-compatible providers)
  • Generates model TOMLs with:
    • Pricing (input/output/cache per million tokens)
    • Context and output limits
    • Capabilities (vision, tools, reasoning, structured output)
    • Modalities (text, image)
    • Status (alpha/beta/deprecated)
  • Auto-detects open weights models (Llama, Qwen, Gemma, etc.)
  • Maps provider IDs and family names to models.dev conventions

Test plan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a capability to export provider and model metadata into a standardized TOML format with per-model files and a summary of outputs.
  • Chores

    • Updated version control ignore rules to exclude generated export outputs.
  • Refactor

    • Cleaned up component imports to remove a duplicate import and streamline module organization.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 23, 2026 00:50
@coderabbitai

coderabbitai Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@steebchen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds exports/ to .gitignore, introduces scripts/export-models-dev.ts (exports provider and per-model TOML files from @llmgateway/models), and small import reordering in apps/ui/src/components/landing/hero-rsc.tsx.

Changes

Cohort / File(s) Summary
Version control
\.gitignore``
Added exports/ ignore rule.
Model export script
\scripts/export-models-dev.ts``
New CLI script that reads providers/models from @llmgateway/models, maps provider/pricing/docs/API/env metadata, formats and escapes values, cleans/creates exports/providers/<provider>/ layout, writes provider.toml, README.md, logo.svg, and nested models/.../*.toml files; logs progress and totals.
UI import cleanup
\apps/ui/src/components/landing/hero-rsc.tsx``
Moved and deduplicated the allMigrations import to the top of the file (import position change only).

Sequence Diagram(s)

sequenceDiagram
  participant CLI as "export-models-dev.ts"
  participant Models as "@llmgateway/models"
  participant FS as "File System (exports/)"
  participant STD as "Stdout"

  CLI->>Models: import providers & models
  Models-->>CLI: provider & model data
  CLI->>CLI: filter/map models, compute fields (family, cost, status, modalities), escape/format values
  CLI->>FS: ensure directories under exports/providers/<provider>/
  CLI->>FS: write provider.toml, README.md, logo.svg
  CLI->>FS: write nested models/.../*.toml files
  CLI->>STD: print progress and summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(scripts): add models.dev export script' directly and clearly describes the main change: adding a new export script for models.dev, which aligns with the primary file addition (scripts/export-models-dev.ts).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@scripts/export-models-dev.ts`:
- Around line 436-447: The code currently calls sanitizeFilename before
splitting, so slashes are removed and nested directory logic never runs; change
to compute rawName = providerMapping?.modelName || model.id, split rawName on
"/" into parts, then sanitize each part (map sanitizeFilename over parts) and
use the sanitized parts to create nestedDir = join(modelsDir,
...sanitizedParts.slice(0, -1)), mkdirSync that directory, and
writeFileSync(join(nestedDir,
`${sanitizedParts[sanitizedParts.length-1]}.toml`), otherwise write into
modelsDir when parts.length === 1; update references to
modelFilename/parts/modelToml accordingly.
🧹 Nitpick comments (2)
scripts/export-models-dev.ts (2)

184-213: Remove unused tomlStringify function.

This function is defined but never used. The script builds TOML output manually in generateProviderToml and generateModelToml instead.

♻️ Proposed fix
-function tomlStringify(obj: Record<string, unknown>, indent = 0): string {
-	const lines: string[] = [];
-	const indentStr = "  ".repeat(indent);
-
-	for (const [key, value] of Object.entries(obj)) {
-		if (value === undefined || value === null) continue;
-
-		if (typeof value === "object" && !Array.isArray(value)) {
-			lines.push(`${indentStr}[${key}]`);
-			lines.push(tomlStringify(value as Record<string, unknown>, 0));
-		} else if (Array.isArray(value)) {
-			const formattedArray = value.map((v) => (typeof v === "string" ? `"${escapeTomlString(v)}"` : v)).join(", ");
-			lines.push(`${indentStr}${key} = [${formattedArray}]`);
-		} else if (typeof value === "string") {
-			lines.push(`${indentStr}${key} = "${escapeTomlString(value)}"`);
-		} else if (typeof value === "boolean") {
-			lines.push(`${indentStr}${key} = ${value}`);
-		} else if (typeof value === "number") {
-			// Format numbers with underscore separators for large values
-			if (Number.isInteger(value) && value >= 1000) {
-				lines.push(`${indentStr}${key} = ${value.toLocaleString("en-US").replace(/,/g, "_")}`);
-			} else {
-				// Format decimal numbers nicely
-				lines.push(`${indentStr}${key} = ${value}`);
-			}
-		}
-	}
-
-	return lines.join("\n");
-}

315-316: Consider documenting the fallback date.

The hardcoded "2024-01-01" fallback when releasedAt is missing could be misleading in the exported data. Consider adding a comment explaining this is a placeholder, or using a more explicit sentinel value if models.dev supports it.

Comment thread scripts/export-models-dev.ts Outdated
Comment on lines +436 to +447
const providerMapping = model.providers.find((p) => p.providerId === providerId);
const modelFilename = sanitizeFilename(providerMapping?.modelName || model.id);

// Handle nested model IDs (e.g., "meta-llama/Meta-Llama-3.1-8B-Instruct")
const parts = modelFilename.split("/");
if (parts.length > 1) {
const nestedDir = join(modelsDir, ...parts.slice(0, -1));
mkdirSync(nestedDir, { recursive: true });
writeFileSync(join(nestedDir, `${parts[parts.length - 1]}.toml`), modelToml);
} else {
writeFileSync(join(modelsDir, `${modelFilename}.toml`), modelToml);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Nested model ID handling is broken.

The sanitizeFilename function (line 390) replaces / with - before the split on line 440. This means parts.length will always be 1, and the nested directory logic (lines 441-444) will never execute.

For model IDs like "meta-llama/Meta-Llama-3.1-8B-Instruct", the current code produces meta-llama-meta-llama-3.1-8b-instruct.toml instead of creating meta-llama/meta-llama-3.1-8b-instruct.toml.

🐛 Proposed fix: Split before sanitizing
-				const modelFilename = sanitizeFilename(providerMapping?.modelName || model.id);
-
-				// Handle nested model IDs (e.g., "meta-llama/Meta-Llama-3.1-8B-Instruct")
-				const parts = modelFilename.split("/");
-				if (parts.length > 1) {
-					const nestedDir = join(modelsDir, ...parts.slice(0, -1));
+				const rawModelName = providerMapping?.modelName || model.id;
+
+				// Handle nested model IDs (e.g., "meta-llama/Meta-Llama-3.1-8B-Instruct")
+				const parts = rawModelName.split("/");
+				const sanitizedParts = parts.map(sanitizeFilename);
+
+				if (sanitizedParts.length > 1) {
+					const nestedDir = join(modelsDir, ...sanitizedParts.slice(0, -1));
 					mkdirSync(nestedDir, { recursive: true });
-					writeFileSync(join(nestedDir, `${parts[parts.length - 1]}.toml`), modelToml);
+					writeFileSync(join(nestedDir, `${sanitizedParts.at(-1)}.toml`), modelToml);
 				} else {
-					writeFileSync(join(modelsDir, `${modelFilename}.toml`), modelToml);
+					writeFileSync(join(modelsDir, `${sanitizedParts[0]}.toml`), modelToml);
 				}
🤖 Prompt for AI Agents
In `@scripts/export-models-dev.ts` around lines 436 - 447, The code currently
calls sanitizeFilename before splitting, so slashes are removed and nested
directory logic never runs; change to compute rawName =
providerMapping?.modelName || model.id, split rawName on "/" into parts, then
sanitize each part (map sanitizeFilename over parts) and use the sanitized parts
to create nestedDir = join(modelsDir, ...sanitizedParts.slice(0, -1)), mkdirSync
that directory, and writeFileSync(join(nestedDir,
`${sanitizedParts[sanitizedParts.length-1]}.toml`), otherwise write into
modelsDir when parts.length === 1; update references to
modelFilename/parts/modelToml accordingly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new TypeScript script to export the repo’s model/provider catalog into models.dev-compatible TOML files, and ignores the generated output directory.

Changes:

  • Add scripts/export-models-dev.ts to generate provider.toml and per-model TOMLs under exports/providers/<provider>/....
  • Add provider metadata mappings (npm package, env vars, docs URL, API base URL) and model metadata mappings (pricing, limits, capabilities).
  • Update .gitignore to exclude the generated exports/ directory.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 4 comments.

File Description
scripts/export-models-dev.ts Implements the models/providers → models.dev TOML export pipeline and filesystem output layout.
.gitignore Ignores generated exports/ output from the export script.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/export-models-dev.ts Outdated
Comment on lines +308 to +310
const inputCost = (providerMapping.inputPrice || 0) * 1e6;
const outputCost = (providerMapping.outputPrice || 0) * 1e6;
const cacheReadCost = providerMapping.cachedInputPrice ? providerMapping.cachedInputPrice * 1e6 : undefined;

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cost calculation ignores providerMapping.discount (documented as a 0-1 discount multiplier in ProviderModelMapping), so exported prices will be higher than the effective prices used elsewhere in this repo (e.g., cheapest-model selection applies the discount). If the models.dev export is meant to reflect real effective pricing, apply the discount multiplier to input/output/cache prices before converting to per-1M-token values.

Suggested change
const inputCost = (providerMapping.inputPrice || 0) * 1e6;
const outputCost = (providerMapping.outputPrice || 0) * 1e6;
const cacheReadCost = providerMapping.cachedInputPrice ? providerMapping.cachedInputPrice * 1e6 : undefined;
const discount = providerMapping.discount ?? 1;
const inputCost = (providerMapping.inputPrice || 0) * discount * 1e6;
const outputCost = (providerMapping.outputPrice || 0) * discount * 1e6;
const cacheReadCost = providerMapping.cachedInputPrice ? providerMapping.cachedInputPrice * discount * 1e6 : undefined;

Copilot uses AI. Check for mistakes.
Comment thread scripts/export-models-dev.ts Outdated
Comment on lines +184 to +213
function tomlStringify(obj: Record<string, unknown>, indent = 0): string {
const lines: string[] = [];
const indentStr = " ".repeat(indent);

for (const [key, value] of Object.entries(obj)) {
if (value === undefined || value === null) continue;

if (typeof value === "object" && !Array.isArray(value)) {
lines.push(`${indentStr}[${key}]`);
lines.push(tomlStringify(value as Record<string, unknown>, 0));
} else if (Array.isArray(value)) {
const formattedArray = value.map((v) => (typeof v === "string" ? `"${escapeTomlString(v)}"` : v)).join(", ");
lines.push(`${indentStr}${key} = [${formattedArray}]`);
} else if (typeof value === "string") {
lines.push(`${indentStr}${key} = "${escapeTomlString(value)}"`);
} else if (typeof value === "boolean") {
lines.push(`${indentStr}${key} = ${value}`);
} else if (typeof value === "number") {
// Format numbers with underscore separators for large values
if (Number.isInteger(value) && value >= 1000) {
lines.push(`${indentStr}${key} = ${value.toLocaleString("en-US").replace(/,/g, "_")}`);
} else {
// Format decimal numbers nicely
lines.push(`${indentStr}${key} = ${value}`);
}
}
}

return lines.join("\n");
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tomlStringify is defined but never used in this script, which adds dead code and increases maintenance surface. Please remove it or refactor the manual TOML building to use it consistently (ensuring the generated format matches models.dev requirements).

Copilot uses AI. Check for mistakes.
Comment thread scripts/export-models-dev.ts Outdated
Comment on lines +389 to +390
// Replace invalid filename characters
return name.replace(/[<>:"/\\|?*]/g, "-").replace(/\s+/g, "-").toLowerCase();

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeFilename replaces "/" with "-", but later code attempts to split on "/" to create nested directories for names like "meta-llama/Meta-Llama-...". As written, parts.length will always be 1, so nested paths are never created and distinct names can collapse into the same filename (risking overwrites). Consider preserving "/" as a path separator (sanitize each segment) or remove the nested-directory logic.

Suggested change
// Replace invalid filename characters
return name.replace(/[<>:"/\\|?*]/g, "-").replace(/\s+/g, "-").toLowerCase();
// Replace invalid filename characters in each path segment, preserving "/" as a separator
return name
.split("/")
.map((segment) => segment.replace(/[<>:"\\|?*]/g, "-").replace(/\s+/g, "-").toLowerCase())
.join("/");

Copilot uses AI. Check for mistakes.
Comment on lines +361 to +367
// Cost section
lines.push("[cost]");
lines.push(`input = ${modelData.cost.input.toFixed(2)}`);
lines.push(`output = ${modelData.cost.output.toFixed(2)}`);
if (modelData.cost.cache_read !== undefined) {
lines.push(`cache_read = ${modelData.cost.cache_read.toFixed(2)}`);
}

Copilot AI Jan 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cost values are forced to 2 decimal places (toFixed(2)), which loses precision for very low per-1M-token prices (e.g., cached input price of 0.004 becomes 0.00). This will produce inaccurate pricing in the exported TOML. Consider emitting more decimal precision (or a trim-to-significant-digits strategy) so small but non-zero prices round-trip correctly.

Copilot uses AI. Check for mistakes.
@steebchen
steebchen force-pushed the steebchen/models-dev-export branch 2 times, most recently from e770e04 to b389711 Compare January 23, 2026 01:09
Add script to export all models and providers to TOML format
compatible with https://github.com/anomalyco/models.dev

Usage: npx tsx scripts/export-models-dev.ts

Features:
- Exports 222 models across 24 providers
- Generates provider.toml with npm package, env vars, docs URL
- Generates model TOMLs with pricing, limits, capabilities
- Auto-detects open weights models (Llama, Qwen, etc.)
- Maps provider IDs and family names to models.dev conventions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@steebchen
steebchen force-pushed the steebchen/models-dev-export branch from b389711 to d877f9e Compare January 23, 2026 01:11
steebchen and others added 4 commits January 23, 2026 01:14
Removes provider subdirectories, exports all models directly
to models/ folder for simpler structure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removes empty generate.ts file and scripts/ directory.
README now links to llmgateway repo for regeneration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
models.dev schema requires limit.output field.
Defaults to 16384 when not specified.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@steebchen
steebchen force-pushed the steebchen/models-dev-export branch from 2276d18 to d48b4ab Compare January 23, 2026 01:25
steebchen and others added 3 commits January 23, 2026 01:27
Maps internal family names to valid models.dev families:
- moonshot → kimi
- bytedance → seed
- zai → glm
- nvidia → nemotron

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Gemma models now use "gemma" family instead of "gemini"
- GPT OSS models now use "gpt-oss" family instead of "gpt"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@steebchen
steebchen force-pushed the steebchen/models-dev-export branch from 5a50abe to b419903 Compare January 23, 2026 01:36
@steebchen
steebchen added this pull request to the merge queue Jan 29, 2026
Merged via the queue into main with commit cdc1b44 Jan 29, 2026
7 checks passed
@steebchen
steebchen deleted the steebchen/models-dev-export branch January 29, 2026 16:57
steebchen added a commit that referenced this pull request Jan 29, 2026
## Summary

Add a script to export all models and providers from
`@llmgateway/models` to TOML format compatible with
[models.dev](https://github.com/anomalyco/models.dev).

## Usage

```bash
npx tsx scripts/export-models-dev.ts
```

## Output

```
exports/providers/
├── openai/
│   ├── provider.toml
│   └── models/
│       ├── gpt-4o.toml
│       └── ...
├── anthropic/
│   ├── provider.toml
│   └── models/
│       └── ...
└── ... (24 providers)
```

## Features

- Exports 222 models across 24 providers
- Generates `provider.toml` with:
  - npm package (`@ai-sdk/openai`, etc.)
  - Environment variables
  - Documentation URL
  - API endpoint (for OpenAI-compatible providers)
- Generates model TOMLs with:
  - Pricing (input/output/cache per million tokens)
  - Context and output limits
  - Capabilities (vision, tools, reasoning, structured output)
  - Modalities (text, image)
  - Status (alpha/beta/deprecated)
- Auto-detects open weights models (Llama, Qwen, Gemma, etc.)
- Maps provider IDs and family names to models.dev conventions

## Test plan

- [x] Script runs successfully
- [x] Output matches models.dev schema
- [x] PR submitted to models.dev:
anomalyco/models.dev#698

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a capability to export provider and model metadata into a
standardized TOML format with per-model files and a summary of outputs.

* **Chores**
* Updated version control ignore rules to exclude generated export
outputs.

* **Refactor**
* Cleaned up component imports to remove a duplicate import and
streamline module organization.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants