Skip to content

Sync new models

Sync new models #351

Workflow file for this run

name: Sync new models
on:
schedule:
- cron: "0 11 * * *"
workflow_dispatch:
inputs:
providers:
description: "Space-separated provider allowlist"
required: false
default: "fireworks openai anthropic baseten xai"
concurrency:
group: sync-models
cancel-in-progress: false
permissions:
contents: write
issues: write
pull-requests: write
jobs:
sync:
runs-on: ubuntu-latest
env:
# Route Claude Code through the Braintrust gateway. The gateway
# authenticates with a Braintrust API key (BRAINTRUST_AGENT_SPEND_API_KEY,
# passed as anthropic_api_key below) and calls the provider on our behalf.
# https://www.braintrust.dev/docs/deploy/gateway
ANTHROPIC_BASE_URL: https://gateway.braintrust.dev
# Attribute gateway spend to the automations-spend-control project.
ANTHROPIC_CUSTOM_HEADERS: "x-bt-project-name: automations-spend-control"
DEFAULT_PROVIDERS: "fireworks openai anthropic baseten xai"
BRANCH_NAME: chore/sync-new-models
COMMIT_MESSAGE: "chore: sync new models"
PR_TITLE: "chore: sync new models"
PR_BODY: "Automated sync of model metadata and newly available models from the approved provider allowlist."
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10.33.0
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Sync selected providers
run: |
set -euo pipefail
providers="${{ github.event.inputs.providers }}"
if [ -z "$providers" ]; then
providers="${DEFAULT_PROVIDERS}"
fi
echo "Syncing providers: $providers"
for provider in $providers; do
pnpm exec tsx packages/proxy/scripts/sync_models.ts update-models --provider "$provider" --write
pnpm exec tsx packages/proxy/scripts/sync_models.ts add-models --provider "$provider" --write
done
- name: Sync Baseten models from /v1/models
env:
BASETEN_API_KEY: ${{ secrets.BASETEN_API_KEY }}
run: |
set -euo pipefail
# Baseten's own /v1/models is authoritative for Baseten availability
# (LiteLLM lags it). Skip cleanly if the key is not configured yet so
# the rest of the sync still runs.
if [ -z "${BASETEN_API_KEY:-}" ]; then
echo "BASETEN_API_KEY not set; skipping Baseten model sync."
exit 0
fi
pnpm exec tsx packages/proxy/scripts/sync_models.ts sync-baseten --write
- name: Sync OpenRouter alternates from /api/v1/models
run: |
set -euo pipefail
# OpenRouter's /api/v1/models is the authoritative source for the
# openrouter provider + pricing/context. This unions `openrouter` into
# models we already carry (matched by stripped canonical id or full
# slug) and adds any model no first-class provider serves as a new
# full-slug entry sunk to the bottom of the catalog. :variant slugs are
# skipped. The endpoint is public, so no API key is required.
pnpm exec tsx packages/proxy/scripts/sync_models.ts sync-openrouter --write
- name: Collect changed model ids
id: changed_models
run: |
set -euo pipefail
git show HEAD:packages/proxy/schema/model_list.json > "$RUNNER_TEMP/model_list.head.json"
pnpm exec tsx packages/proxy/scripts/collect_changed_model_ids.ts \
--before "$RUNNER_TEMP/model_list.head.json" \
--after "packages/proxy/schema/model_list.json" \
--output "$RUNNER_TEMP/sync-model-changed.json" \
--github-output "$GITHUB_OUTPUT"
- name: Enrich changed model metadata with Claude Code
if: ${{ steps.changed_models.outputs.count != '0' }}
timeout-minutes: 15
uses: anthropics/claude-code-action@fbda2eb1bdc90d319b8d853f5deb53bca199a7c1 # v1.0.140
with:
anthropic_api_key: ${{ secrets.BRAINTRUST_AGENT_SPEND_API_KEY }}
github_token: ${{ github.token }}
display_report: "true"
claude_args: |
--model claude-opus-4-6
--max-turns 80
--allowedTools "Read,Glob,Grep,LS,WebSearch,WebFetch,Edit,Write"
--disallowedTools "Bash,MultiEdit,Replace,NotebookEditCell,mcp__github__create_issue,mcp__github__create_issue_comment,mcp__github__update_issue,mcp__github__create_pr,mcp__github__create_or_update_file,mcp__github__delete_file,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files"
prompt: |
# Goal
After the deterministic `sync_models.ts` update/add passes finish, enrich the model entries changed on this branch by this run with verified `displayName` metadata and, only when appropriate, verified `parent` metadata.
# Inputs
- Changed model id file: `${{ runner.temp }}/sync-model-changed.json`
- Changed model count: `${{ steps.changed_models.outputs.count }}`
# Files to inspect first
- `packages/proxy/schema/model_list.json`
- `packages/proxy/schema/models.ts`
- `packages/proxy/scripts/sync_models.ts`
- `.github/workflows/sync-models.yaml`
# What to do
1. Read the changed model id file and only work on those exact model ids.
2. For each changed model, read its current entry in `packages/proxy/schema/model_list.json` and note whether `displayName` and `parent` are already set.
3. For fields that are already set, verify the existing value against official provider sources before deciding whether to keep or correct it. Do not go to the web first — read the local value first, then verify.
4. Update only `packages/proxy/schema/model_list.json`.
5. Preserve existing JSON formatting, field order, and neighboring naming conventions.
6. Only modify `displayName` and `parent`. Do not change pricing, token limits, providers, deprecation fields, format, flavor, or capability flags in this step.
6a. When updating `displayName` or `parent`, replace any existing key in place. Never leave duplicate JSON properties in a model entry.
7. Add a human-friendly `displayName` when it is missing, or correct it if the existing value is clearly inconsistent with official provider naming.
8. Set `parent` only when official sources explicitly document a relationship between this model id and a stable base alias, and the parent model id already exists in `model_list.json`. Do not set `parent` based on inference alone.
9. Consider `parent` for any of these verified relationship patterns — check official docs for each:
- **Dated snapshot → stable alias**: model id ends with a date or version suffix and official docs name a stable alias (e.g. `claude-3-5-sonnet-20241022` → `claude-3-5-sonnet-latest`, `gpt-4o-2024-11-20` → `gpt-4o`, `gemini-2.0-flash-001` → `gemini-2.0-flash`)
- **Versioned snapshot**: model id ends with `@NNN` or `-vN` and official docs point to a base model (e.g. `publishers/google/models/gemini-pro@001` → `publishers/google/models/gemini-pro`)
- **Location-scoped variant**: model id starts with a location prefix (`global.`, `us.`, `eu.`, `apac.`) and official docs point to a non-location parent
- **Tier or access variant**: model id has a suffix that denotes a serving tier documented by the provider as a variant of a base model (e.g. `:free`, `:nitro`, `:floor` on Together AI)
- **Any other explicitly documented alias-to-base relationship** in official provider API or model docs
10. Leave `parent` unset when:
- the model is a base model with no documented parent
- the relationship cannot be verified from official sources
- the parent model id does not exist in the local `model_list.json`
11. If a field cannot be verified confidently from official sources, leave it unset instead of guessing.
# Display name examples
**Good** (model id → `displayName`):
- `"mistral-small-latest"` → `"Mistral Small"` — stable alias, no date suffix, no version number
- `"mistral-large-2512"` → `"Mistral Large 3 (2512)"` — version "3" confirmed in Mistral's official release docs; YYMM kept as-is in parentheses
- `"claude-3-5-sonnet-20241022"` → `"Claude 3.5 Sonnet (2024-10-22)"` — ISO date in parentheses, matches official Anthropic naming exactly
- `"gpt-4o-2024-11-20"` → `"GPT-4o (2024-11-20)"` — ISO date in parentheses, provider spells it "GPT-4o"
**Bad** (what NOT to do):
- `"mistral-small-2603"` → `"Mistral Small 4 (2603)"` ❌ — "4" is a generation number not found in the model id or official Mistral docs for this snapshot; if official docs do not confirm the generation label, omit it and write `"Mistral Small (2603)"` instead
- `"mistral-small-2603"` → `"Mistral Small (March 2026)"` ❌ — never expand a YYMM code into prose month/year; keep the original numeric form in parentheses
- `"claude-3-5-sonnet-20241022"` → `"Claude 3.5 Sonnet October 2024"` ❌ — date must be in parentheses, not free prose
# Constraints
- Do not edit any file other than `packages/proxy/schema/model_list.json`.
- Use official provider sources only for metadata decisions.
- Do not invent placeholder values.
- name: Canonicalize model list and equivalent models after metadata enrichment
if: ${{ steps.changed_models.outputs.count != '0' }}
run: pnpm exec tsx packages/proxy/scripts/sync_models.ts normalize-local-models --write
- name: Check expected files only
id: changes
run: |
set -euo pipefail
if [ -z "$(git status --short)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
unexpected=$(git status --porcelain | awk '{print $2}' | grep -vE '^(packages/proxy/schema/model_list\.json|packages/proxy/schema/index\.ts)$' || true)
if [ -n "$unexpected" ]; then
echo "Unexpected files modified:"
echo "$unexpected"
exit 1
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
- name: Build proxy package
if: ${{ steps.changes.outputs.changed == 'true' }}
run: pnpm --filter @braintrust/proxy run build
- name: Validate model schema
if: ${{ steps.changes.outputs.changed == 'true' }}
run: pnpm exec vitest run packages/proxy/schema/models.test.ts packages/proxy/schema/index.test.ts packages/proxy/scripts/sync_models.test.ts packages/proxy/scripts/collect_changed_model_ids.test.ts
- name: Create PR
id: pr
if: ${{ steps.changes.outputs.changed == 'true' }}
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
token: ${{ github.token }}
base: main
branch: ${{ env.BRANCH_NAME }}
commit-message: ${{ env.COMMIT_MESSAGE }}
title: ${{ env.PR_TITLE }}
body: ${{ env.PR_BODY }}
labels: auto-sync
signoff: false