Merge pull request #38257 from BerriAI/litellm_together_registry_sync

feat(models): add daily Together AI model registry sync script and workflow
This commit is contained in:
Mateo Wang 2026-08-27 19:46:37 -07:00 committed by GitHub
commit 5337c68dd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1401 additions and 0 deletions

View file

@ -0,0 +1,68 @@
name: Sync Together AI model registry
on:
schedule:
- cron: "30 6 * * *"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
sync_together_ai_models:
if: github.repository == 'BerriAI/litellm'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: litellm_internal_staging
persist-credentials: false
- name: Set up uv
uses: ./.github/actions/setup-uv-with-retries
with:
version: "0.10.9"
- name: Look for an already-open sync PR
id: existing
run: |
open_pr="$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 1000 --json headRefName \
--jq '[.[].headRefName | select(startswith("litellm_together_registry_sync_"))] | first // empty')"
echo "open_pr=$open_pr" >> "$GITHUB_OUTPUT"
if [ -n "$open_pr" ]; then
echo "An open sync PR already exists on branch $open_pr; skipping this run."
fi
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Run the sync
if: steps.existing.outputs.open_pr == ''
run: |
uv run --frozen python scripts/sync_together_ai_models.py --write --pr-body-file "$RUNNER_TEMP/pr_body.md"
env:
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
- name: Regenerate the JSON schema
if: steps.existing.outputs.open_pr == ''
run: |
uv run --frozen python ci_cd/generate_model_prices_schema.py
- name: Create a pull request when the registry changed
if: steps.existing.outputs.open_pr == ''
run: |
if git diff --quiet; then
echo "Registry already in sync; no PR needed."
exit 0
fi
branch="litellm_together_registry_sync_$(date +'%Y-%m-%d')"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
git add model_prices_and_context_window.json \
litellm/model_prices_and_context_window_backup.json \
model_prices_and_context_window.schema.json
git commit -m "feat(models): sync together_ai model registry $(date +'%Y-%m-%d')"
gh auth setup-git
git push origin "$branch"
gh pr create --title "feat(models): sync together_ai model registry" \
--body-file "$RUNNER_TEMP/pr_body.md" \
--head "$branch" \
--base litellm_internal_staging
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}

View file

@ -0,0 +1,539 @@
"""Sync the together_ai entries of model_prices_and_context_window.json with Together's live serverless catalog.
Pulls ``GET https://api.together.ai/v1/models?serverless`` plus the deprecations doc, maps API fields onto
registry fields, merges the reviewed capability rules below for everything the API cannot express, and diffs
the result against the registry. Dry run (the default) prints the diff summary and the generated PR body;
``--write`` applies the changes to the root cost map and its ``litellm/`` backup copy.
Policy highlights:
- Prices arrive per 1M tokens with float artifacts and are normalized to clean per-token values.
- A registry entry absent from the serverless catalog is marked with ``deprecation_date`` from the docs
deprecation table, never deleted; absences with no docs date are surfaced for a human call.
- Availability comes from the API: a model the docs list as removed but the API still serves stays live,
with the conflict surfaced as a warning.
- Manually curated values the API cannot express (``metadata.successor``, ``max_output_tokens`` on existing
entries, capability flags no rule covers) are never overwritten; conflicts are surfaced instead.
"""
import argparse
import json
import os
import re
import sys
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from types import MappingProxyType
from typing import Final
import httpx
from pydantic import BaseModel, TypeAdapter, ValidationError
MODELS_URL: Final = "https://api.together.ai/v1/models?serverless"
DEPRECATIONS_URL: Final = "https://docs.together.ai/docs/deprecations.md"
PROVIDER: Final = "together_ai"
PREFIX: Final = "together_ai/"
SOURCE_URL: Final = "https://docs.together.ai/docs/serverless-models"
COST_MAP_RELPATHS: Final = (
"model_prices_and_context_window.json",
"litellm/model_prices_and_context_window_backup.json",
)
TYPE_TO_MODE: Final = MappingProxyType({"chat": "chat", "embedding": "embedding", "moderation": "chat"})
class SyncError(RuntimeError):
pass
class CatalogPricing(BaseModel):
input: float
output: float
cached_input: float | None = None
class CatalogModel(BaseModel):
id: str
type: str
context_length: int | None = None
pricing: CatalogPricing
CATALOG_ADAPTER: Final = TypeAdapter(list[CatalogModel])
RegistryEntry = dict[str, object]
CostMap = dict[str, object]
@dataclass(frozen=True, slots=True)
class CapabilityRule:
model_id: str
fields: Mapping[str, bool | int]
provenance: str
def _rule(model_id: str, provenance: str, **fields: bool | int) -> CapabilityRule:
return CapabilityRule(model_id=model_id, fields=MappingProxyType(dict(fields)), provenance=provenance)
_TOOLS: Final = MappingProxyType(
{
"supports_function_calling": True,
"supports_parallel_function_calling": True,
"supports_response_schema": True,
"supports_tool_choice": True,
}
)
CAPABILITY_RULES: Final = (
_rule(
"MiniMaxAI/MiniMax-M3",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/minimax-m3",
**_TOOLS,
supports_reasoning=True,
supports_vision=True,
),
_rule("Prism-ML/Ternary-Bonsai-27B", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"Qwen/Qwen3.5-9B",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/qwen3-5-9b",
**_TOOLS,
supports_reasoning=True,
supports_vision=True,
),
_rule(
"Qwen/Qwen3.6-Plus",
"reviewed for the LIT-5968 backfill; hybrid reasoning model without a documented tools contract",
supports_reasoning=True,
),
_rule("Qwen/Qwen3.7-Max", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule("Qwen/Qwen3.7-Plus", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule("Qwen/Qwen3.8-2.4T-A95B", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule("arize-ai/qwen-2-1.5b-instruct", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"deepseek-ai/DeepSeek-V4-Flash-0731",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/deepseek-v4-flash",
**_TOOLS,
),
_rule(
"deepseek-ai/DeepSeek-V4-Pro",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/deepseek-v4-pro",
**_TOOLS,
supports_reasoning=True,
),
_rule(
"deepseek-ai/DeepSeek-V4-Pro-0813",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/deepseek-v4-pro",
**_TOOLS,
),
_rule("google/gemma-3n-E4B-it", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"google/gemma-4-31B-it",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/gemma-4-31b-it",
**_TOOLS,
supports_vision=True,
),
_rule(
"intfloat/multilingual-e5-large-instruct",
"embedding dims per https://huggingface.co/intfloat/multilingual-e5-large-instruct",
output_vector_size=1024,
),
_rule(
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
"reviewed for the LIT-5968 backfill against https://docs.together.ai/docs/function-calling",
**_TOOLS,
),
_rule(
"meta-llama/Llama-Guard-4-12B",
"moderation classifier with a chat-shaped API; no tools per the LIT-5968 backfill review",
),
_rule("meta-models/Muse-Glimmer-30B", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"moonshotai/Kimi-K2.7-Code",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/kimi-k2-7-code",
**_TOOLS,
supports_vision=True,
),
_rule(
"moonshotai/Kimi-K3",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/kimi-k3",
**_TOOLS,
supports_reasoning=True,
supports_vision=True,
),
_rule(
"nvidia/nemotron-3-ultra-550b-a55b",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/nemotron-3-ultra",
**_TOOLS,
supports_reasoning=True,
),
_rule(
"openai/gpt-oss-120b",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/gpt-oss-120b",
**_TOOLS,
supports_reasoning=True,
),
_rule(
"openai/gpt-oss-20b",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/gpt-oss-20b",
**_TOOLS,
),
_rule("pearl-ai/gemma-4-31b-it", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"thinkingmachines/Inkling",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/inkling",
**_TOOLS,
),
_rule("thinkingmachines/Inkling-Small", "reviewed for the LIT-5968 backfill; no tool or vision support documented"),
_rule(
"zai-org/GLM-5.2",
"reviewed for the LIT-5968 backfill against https://www.together.ai/models/glm-5-2",
**_TOOLS,
supports_reasoning=True,
),
)
RULES_BY_ID: Final = MappingProxyType({rule.model_id: rule for rule in CAPABILITY_RULES})
@dataclass(frozen=True, slots=True)
class DeprecationDoc:
removal_dates: Mapping[str, str]
redirects: Mapping[str, str]
_REDIRECT_ROW: Final = re.compile(r"^\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|")
_REMOVAL_ROW: Final = re.compile(r"^\|\s*(\d{4}-\d{2}-\d{2})\s*\|\s*`([^`]+)`\s*\|")
def _section(markdown: str, heading: str) -> str:
level: Final = heading.split(" ", 1)[0]
start: Final = markdown.find(f"\n{heading}\n")
if start < 0:
return ""
body: Final = markdown[start + 1 + len(heading) :]
next_heading: Final = re.search(rf"^{re.escape(level)} ", body, flags=re.MULTILINE)
return body[: next_heading.start()] if next_heading else body
def parse_deprecations(markdown: str) -> DeprecationDoc:
redirect_rows: Final = tuple(
m.groups()
for m in (_REDIRECT_ROW.match(line) for line in _section(markdown, "## Active model redirects").splitlines())
if m
)
inference: Final = _section(_section(markdown, "## Deprecation history"), "### Inference")
removal_rows: Final = tuple(m.groups() for m in (_REMOVAL_ROW.match(line) for line in inference.splitlines()) if m)
if not redirect_rows or not removal_rows:
raise SyncError(
"deprecations doc parsed to zero redirect or removal rows; the table format at "
f"{DEPRECATIONS_URL} changed and the parser needs updating"
)
removal_dates: Final = {model: date for date, model in reversed(removal_rows)}
return DeprecationDoc(
removal_dates=MappingProxyType(dict(reversed(removal_dates.items()))),
redirects=MappingProxyType({original: target for original, target in redirect_rows}),
)
def per_token(price_per_million: float) -> float:
return float(f"{price_per_million / 1e6:.6g}")
def _resolve_name(name: str, universe: frozenset[str]) -> str | None:
if name in universe:
return name
suffix_matches: Final = tuple(candidate for candidate in universe if candidate.endswith(f"/{name}"))
return suffix_matches[0] if len(suffix_matches) == 1 else None
def resolve_successor(model_id: str, doc: DeprecationDoc, live_ids: frozenset[str]) -> str | None:
canonical: Final = live_ids | frozenset(doc.removal_dates)
redirects: Final = {
(_resolve_name(raw_source, canonical) or raw_source): (_resolve_name(raw_target, canonical) or raw_target)
for raw_source, raw_target in doc.redirects.items()
}
seen: Final = set()
current = model_id # rebind-ok: walks the redirect chain
while current in redirects and current not in seen:
seen.add(current)
current = redirects[current] # rebind-ok: walks the redirect chain
return current if current != model_id and current in live_ids else None
@dataclass(frozen=True, slots=True)
class SyncOutcome:
cost_map: CostMap
added: tuple[str, ...] = ()
updated: tuple[str, ...] = ()
deprecated: tuple[str, ...] = ()
reappeared: tuple[str, ...] = ()
warnings: tuple[str, ...] = ()
skipped_types: Mapping[str, int] = field(default_factory=dict)
@property
def has_changes(self) -> bool:
return bool(self.added or self.updated or self.deprecated or self.reappeared)
def _api_fields(model: CatalogModel) -> RegistryEntry:
cached: Final = model.pricing.cached_input
return {
"input_cost_per_token": per_token(model.pricing.input),
"output_cost_per_token": per_token(model.pricing.output),
**({"cache_read_input_token_cost": per_token(cached), "supports_prompt_caching": True} if cached else {}),
**({"max_input_tokens": model.context_length} if model.context_length is not None else {}),
}
def _new_entry(model: CatalogModel, mode: str) -> RegistryEntry:
rule: Final = RULES_BY_ID.get(model.id)
length_fields: Final = (
{}
if model.context_length is None
else {"max_input_tokens": model.context_length, "max_tokens": model.context_length}
| ({"max_output_tokens": model.context_length} if mode == "chat" else {})
)
merged: Final = {
**_api_fields(model),
**length_fields,
"litellm_provider": PROVIDER,
"mode": mode,
"source": SOURCE_URL,
**(dict(rule.fields) if rule else {}),
}
return dict(sorted(merged.items()))
def _updated_entry(entry: RegistryEntry, model: CatalogModel) -> tuple[RegistryEntry, tuple[str, ...]]:
rule: Final = RULES_BY_ID.get(model.id)
desired: Final = {**_api_fields(model), **(dict(rule.fields) if rule else {})}
dropped: Final = () if model.pricing.cached_input else ("cache_read_input_token_cost", "supports_prompt_caching")
changes: Final = tuple(
f"{name}: {entry.get(name)!r} -> {value!r}" for name, value in desired.items() if entry.get(name) != value
) + tuple(
f"{name}: {entry[name]!r} removed (no longer in the catalog pricing)" for name in dropped if name in entry
)
merged: Final = {name: value for name, value in {**entry, **desired}.items() if name not in dropped}
return dict(sorted(merged.items())), changes
def _with_new_keys_in_block(original: CostMap, result: CostMap, new_keys: Sequence[str]) -> CostMap:
provider_keys: Final = tuple(key for key in original if key.startswith(PREFIX))
if not new_keys or not provider_keys:
return result
block_end: Final = provider_keys[-1]
return {
key: value
for existing in original
for key, value in (
(existing, result[existing]),
*((new, result[new]) for new in sorted(new_keys) if existing == block_end),
)
}
def compute_sync(cost_map: CostMap, catalog: Sequence[CatalogModel], doc: DeprecationDoc) -> SyncOutcome:
live_ids: Final = frozenset(model.id for model in catalog)
token_models: Final = {model.id: model for model in catalog if model.type in TYPE_TO_MODE}
skipped: Final = {
model.type: sum(1 for m in catalog if m.type == model.type)
for model in catalog
if model.type not in TYPE_TO_MODE
}
registry_ids: Final = {key.removeprefix(PREFIX): key for key in cost_map if key.startswith(PREFIX)}
added: Final[list[str]] = []
updated: Final[list[str]] = []
deprecated: Final[list[str]] = []
reappeared: Final[list[str]] = []
warnings: Final[list[str]] = []
result: Final[CostMap] = dict(cost_map)
for model_id, model in sorted(token_models.items()):
mode: Final = TYPE_TO_MODE[model.type]
key: Final = f"{PREFIX}{model_id}"
if model_id in doc.removal_dates:
warnings.append(
f"`{key}` is listed as removed on {doc.removal_dates[model_id]} in the docs but the serverless "
"catalog still serves it; availability kept from the API"
)
entry = result.get(key)
if not isinstance(entry, dict):
result[key] = _new_entry(model, mode)
added.append(key)
if model.type == "chat" and model_id not in RULES_BY_ID:
warnings.append(
f"`{key}` added without a capability rule; review its tools/vision/reasoning support and add one"
)
continue
if entry.get("mode") != mode:
warnings.append(
f"`{key}` has curated mode {entry.get('mode')!r} but the catalog maps to {mode!r}; left unchanged"
)
new_entry, changes = _updated_entry(entry, model)
if "deprecation_date" in new_entry:
new_entry.pop("deprecation_date")
reappeared.append(key)
if changes:
updated.append(f"{key}: " + "; ".join(changes))
if changes or key in reappeared:
result[key] = new_entry
for model_id, key in sorted(registry_ids.items()):
if model_id in token_models:
continue
entry = result.get(key)
if not isinstance(entry, dict):
continue
removal_date: Final = doc.removal_dates.get(model_id)
successor: Final = resolve_successor(model_id, doc, live_ids)
metadata = entry.get("metadata")
curated_successor: Final = metadata.get("successor") if isinstance(metadata, dict) else None
new_entry = dict(entry)
if removal_date is not None and entry.get("deprecation_date") != removal_date:
if "deprecation_date" in entry:
warnings.append(
f"`{key}` has curated deprecation_date {entry.get('deprecation_date')!r} but the docs list "
f"{removal_date!r}; left unchanged"
)
else:
new_entry["deprecation_date"] = removal_date
if removal_date is None and "deprecation_date" not in entry:
warnings.append(
f"`{key}` is absent from the serverless catalog with no removal date in the docs; "
"needs a human deprecation call"
)
if successor is not None:
desired_successor: Final = f"{PREFIX}{successor}"
if curated_successor is None:
new_entry["metadata"] = dict(
sorted({**(metadata if isinstance(metadata, dict) else {}), "successor": desired_successor}.items())
)
elif curated_successor != desired_successor:
warnings.append(
f"`{key}` has curated successor {curated_successor!r} but the docs redirects resolve to "
f"{desired_successor!r}; left unchanged"
)
if new_entry != entry:
result[key] = dict(sorted(new_entry.items()))
deprecated.append(f"{key}: " + ", ".join(sorted(set(new_entry) - set(entry)) or ["updated"]))
return SyncOutcome(
cost_map=_with_new_keys_in_block(cost_map, result, tuple(added)),
added=tuple(added),
updated=tuple(updated),
deprecated=tuple(deprecated),
reappeared=tuple(reappeared),
warnings=tuple(warnings),
skipped_types=MappingProxyType(skipped),
)
def _section_block(title: str, lines: Sequence[str], backtick: bool) -> str:
bullets: Final = "\n".join(f"- `{line}`" if backtick else f"- {line}" for line in lines) or "- none"
return f"### {title} ({len(lines)})\n{bullets}\n"
def render_pr_body(outcome: SyncOutcome) -> str:
skipped: Final = ", ".join(f"{kind} ({count})" for kind, count in sorted(outcome.skipped_types.items())) or "none"
return (
"Automated daily sync of the together_ai entries in model_prices_and_context_window.json against "
f"`GET {MODELS_URL}` and {DEPRECATIONS_URL} by scripts/sync_together_ai_models.py.\n"
"\n"
f"{_section_block('Added', outcome.added, backtick=True)}"
"\n"
f"{_section_block('Updated', outcome.updated, backtick=True)}"
"\n"
f"{_section_block('Marked deprecated', outcome.deprecated, backtick=True)}"
"\n"
f"{_section_block('Returned to the catalog', outcome.reappeared, backtick=True)}"
"\n"
f"{_section_block('Warnings needing a human call', outcome.warnings, backtick=False)}"
"\n"
f"Catalog model types outside the sync's token-pricing scope, skipped: {skipped}\n"
)
def render_summary(outcome: SyncOutcome) -> str:
return (
f"added={len(outcome.added)} updated={len(outcome.updated)} deprecated={len(outcome.deprecated)} "
f"reappeared={len(outcome.reappeared)} warnings={len(outcome.warnings)}"
)
def load_catalog(raw: bytes) -> list[CatalogModel]:
parsed: Final = json.loads(raw)
entries: Final = parsed.get("data") if isinstance(parsed, dict) else parsed
try:
catalog: Final = CATALOG_ADAPTER.validate_python(entries)
except ValidationError as error:
raise SyncError(f"the catalog response no longer matches the expected shape: {error}") from error
if not any(model.type in TYPE_TO_MODE for model in catalog):
raise SyncError(
"the catalog response contains no token-priced models; refusing to mark the whole registry deprecated"
)
return catalog
def _fetch(url: str, headers: Mapping[str, str]) -> bytes:
response: Final = httpx.get(url, headers=dict(headers), timeout=30, follow_redirects=True)
if response.status_code != 200:
raise SyncError(f"GET {url} returned {response.status_code}")
return response.content
def _serialize(cost_map: CostMap) -> str:
return json.dumps(cost_map, indent=4, ensure_ascii=False) + "\n"
def main(argv: Sequence[str]) -> int:
parser: Final = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true", help="apply the sync to the cost map files (default: dry run)")
parser.add_argument("--models-json", type=Path, help="recorded catalog response to use instead of the live API")
parser.add_argument(
"--deprecations-md", type=Path, help="recorded deprecations doc to use instead of the live docs"
)
parser.add_argument("--pr-body-file", type=Path, help="write the generated PR body to this path")
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parent.parent)
args: Final = parser.parse_args(argv)
if args.models_json is not None:
catalog_raw: Final = args.models_json.read_bytes()
else:
api_key: Final = os.environ.get("TOGETHER_API_KEY")
if not api_key:
raise SyncError("TOGETHER_API_KEY is not set and --models-json was not given")
catalog_raw = _fetch(MODELS_URL, {"Authorization": f"Bearer {api_key}"}) # rebind-ok: branch-dependent source
catalog: Final = load_catalog(catalog_raw)
markdown: Final = (
args.deprecations_md.read_text() if args.deprecations_md is not None else _fetch(DEPRECATIONS_URL, {}).decode()
)
doc: Final = parse_deprecations(markdown)
cost_map_path: Final = args.repo_root / COST_MAP_RELPATHS[0]
cost_map: Final = json.loads(cost_map_path.read_text())
outcome: Final = compute_sync(cost_map, catalog, doc)
body: Final = render_pr_body(outcome)
if args.pr_body_file is not None:
args.pr_body_file.write_text(body)
if args.write and outcome.has_changes:
for relpath in COST_MAP_RELPATHS:
(args.repo_root / relpath).write_text(_serialize(outcome.cost_map))
print(render_summary(outcome))
print()
print(body)
if not args.write:
print("dry run: no files were touched")
elif not outcome.has_changes:
print("registry already in sync: no files were touched")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main(sys.argv[1:]))
except SyncError as error:
print(f"SYNC FAILED: {error}", file=sys.stderr)
raise SystemExit(1) from error

View file

@ -0,0 +1,442 @@
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.together.ai/llms.txt
> Use this file to discover all available pages before exploring further.
# Deprecations
> Together AI's model lifecycle policy, including upgrades, redirects, and deprecation schedules.
Together AI regularly updates the platform with new open-source models. This page describes the model lifecycle policy and lists active redirects and scheduled deprecations.
## Model lifecycle policy
Together AI follows a structured approach to introducing new models, upgrading existing models, and deprecating older versions, so you can rely on predictable behavior.
### Model upgrades (redirects)
An **upgrade** is a model release that is materially the same model lineage with targeted improvements and no fundamental changes to how developers use or reason about it.
A model qualifies as an upgrade when **one or more** of the following are true (and none of the "new model" criteria apply):
* Same modality and task profile (e.g., instruct → instruct, reasoning → reasoning).
* Same architecture family (e.g., DeepSeek-V3 → DeepSeek-V3-0324).
* Post-training or fine-tuning improvements, bug fixes, safety tuning, or small data refresh.
* Behavior is strongly compatible (prompting patterns and evals are similar).
* Pricing change is none or small (≤10% increase).
**Outcome:** The current endpoint redirects to the upgraded version after a **3-day notice**. The old version remains available via dedicated endpoints.
### New models (no redirect)
A **new model** is a release with materially different capabilities, costs, or operating characteristics, so a silent redirect would be misleading.
Any of the following triggers classification as a new model:
* Modality shift (e.g., reasoning-only ↔ instruct/hybrid, text → multimodal).
* Architecture shift (e.g., Qwen3 → Qwen3-Next, Llama 3 → Llama 4).
* Large behavior shift (prompting patterns, output style, or verbosity materially different).
* Experimental flag by provider (e.g., DeepSeek-V3-Exp).
* Large price change (>10% increase or pricing structure change).
* Benchmark deltas that meaningfully change task positioning.
* Safety policy or system prompt changes that noticeably affect outputs.
**Outcome:** No automatic redirect. Together AI announces the new model and deprecates the old one on a **2-week timeline** (both are available during this window). You must explicitly switch model IDs.
## Active model redirects
The following models are redirected to newer versions. Requests to the original model ID are automatically routed to the upgraded version:
| Original model | Redirects to | Notes |
| :----------------------------------- | :---------------------------------------- | :---------------------------------------- |
| `mistralai/Mistral-7B-Instruct-v0.3` | `mistralai/Ministral-3-14B-Instruct-2512` | Same lineage, upgraded version |
| `Kimi-K2` | `Kimi-K2-0905` | Same architecture, improved post-training |
| `DeepSeek-V3` | `DeepSeek-V3.1` | Same architecture, targeted improvements |
| `DeepSeek-V3-0324` | `DeepSeek-V3.1` | Same architecture, targeted improvements |
| `DeepSeek-R1` | `DeepSeek-R1-0528` | Same architecture, targeted improvements |
<Tip>
If you need to use the original model version, you can always deploy it as a [dedicated endpoint](/docs/dedicated-endpoints).
</Tip>
## Deprecation policy
| Model type | Deprecation notice | Notes |
| :--------------------------- | :---------------------------------- | :------------------------------------------------------- |
| Preview model | \<24 hours of notice, after 30 days | Clearly marked in docs and playground with "Preview" tag |
| Serverless endpoint | 2 or 3 weeks\* | |
| On-demand dedicated endpoint | 2 or 3 weeks\* | |
\*Depends on usage and whether a newer version of the model is available.
* If you use a model scheduled for deprecation, you receive an email notification.
* All changes appear on this page.
* Each deprecated model has a specified removal date.
* After the removal date, the model is no longer available via its serverless endpoint, but migration options are described below.
## Migration options
When a model is deprecated on the serverless platform, you have three options:
1. **On-demand dedicated endpoint** (if supported):
* Reserved solely for you. You choose the underlying hardware.
* Charged on a price-per-minute basis.
* Endpoints can be dynamically spun up and down.
2. **Monthly reserved dedicated endpoint:**
* Reserved solely for you.
* Charged on a month-by-month basis.
* Can be requested via this [form](https://together.ai/monthly-reserved).
3. **Migrate to a newer serverless model:**
* Switch to an updated model on the serverless platform.
## Migration steps
1. Review the deprecation table below to find your current model.
2. Check if on-demand dedicated endpoints are supported for your model.
3. Decide on your preferred migration option.
4. If you choose a new serverless model, test your application thoroughly before migrating.
5. Update your API calls to use the new model or dedicated endpoint.
## Deprecation history
### Inference
The table below lists all models removed from serverless inference, most recent first.
| Removal date | Model | Supported by on-demand dedicated endpoints |
| :-------------------------- | :-------------------------------------------------- | :----------------------------------------- |
| 2026-08-21 | `deepcogito/cogito-v2-1-671b` | No |
| 2026-08-04 | `google/gemma-3n-E4B-it` | No |
| 2026-07-10 | `Qwen/Qwen3-235B-A22B-Instruct-2507-tput` | Yes |
| 2026-07-10 | `meta-llama/Meta-Llama-3-8B-Instruct-Lite` | No |
| 2026-07-10 | `zai-org/GLM-5.1` | Yes |
| 2026-06-29 | `Qwen/Qwen3.5-397B-A17B` | Yes |
| 2026-06-22 | `zai-org/GLM-5` | No |
| 2026-06-11 | `mistralai/Voxtral-Mini-3B-2507` | No |
| 2026-06-04 | `Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8` | Yes |
| 2026-05-27 | `black-forest-labs/FLUX.1-krea-dev` | No |
| 2026-05-21 | `moonshotai/Kimi-K2.5` | No |
| 2026-05-14 | `deepseek-ai/DeepSeek-R1` | No |
| 2026-05-14 | `deepseek-ai/DeepSeek-V3.1` | Yes |
| 2026-05-14 | `Qwen/Qwen3-Coder-Next-FP8` | Yes |
| 2026-04-16 | `Qwen/Qwen3-VL-8B-Instruct` | Yes |
| 2026-04-16 | `Qwen/Qwen3-235B-A22B-Thinking-2507` | Yes |
| 2026-04-16 | `mistralai/Mixtral-8x7B-Instruct-v0.1` | Yes |
| 2026-04-03 | `ServiceNow-AI/Apriel-1.5-15b-Thinker` | No |
| 2026-04-03 | `ServiceNow-AI/Apriel-1.6-15b-Thinker` | No |
| 2026-04-02 | `zai-org/GLM-4.5-Air-FP8` | No |
| 2026-04-02 | `zai-org/GLM-4.7` | No |
| 2026-04-02 | `mistralai/Mistral-Small-24B-Instruct-2501` | No |
| 2026-04-02 | `Qwen/Qwen3-Next-80B-A3B-Instruct` | Yes |
| 2026-03-31 | `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | Yes |
| 2026-03-06 | `mixedbread-ai/Mxbai-Rerank-Large-V2` | No |
| 2026-03-06 | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | Yes |
| 2026-03-06 | `Qwen/Qwen3-235B-A22B-Thinking-2507` | Yes |
| 2026-03-06 | `moonshotai/Kimi-K2-Thinking` | No |
| 2026-03-06 | `moonshotai/Kimi-K2-Instruct-0905` | No |
| 2026-03-06 | `meta-llama/Llama-3.2-3B-Instruct-Turbo` | No |
| 2026-02-25 | `black-forest-labs/FLUX.1-dev` | No |
| 2026-02-25 | `black-forest-labs/FLUX.1-dev-lora` | No |
| 2026-02-25 | `black-forest-labs/FLUX.1-Kontext-dev` | No |
| 2026-02-25 | `Qwen/Qwen3-VL-32B-Instruct` | No |
| 2026-02-25 | `meta-llama/Llama-3.2-3B-Instruct-Turbo-Classifier` | No |
| 2026-02-25 | `mistralai/Ministral-3-14B-Instruct` | No |
| 2026-02-25 | `Qwen/Qwen3-Next-80B-A3B-Thinking` | No |
| 2026-02-25 | `Alibaba-NLP/gte-modernbert-base` | No |
| 2026-02-25 | `BAAI/bge-base-en-v1.5-vllm` | No |
| 2026-02-25 | `meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo` | No |
| 2026-02-25 | `meta-llama/Llama-Guard-3-11B-Vision-Turbo` | No |
| 2026-02-25 | `meta-llama/LlamaGuard-2-8b` | No |
| 2026-02-25 | `marin-community/Marin-8B-Instruct` | No |
| 2026-02-25 | `nvidia/Nvidia-Nemotron-Nano-9B-v2` | No |
| 2026-02-06 | `togethercomputer/m2-bert-80M-32k-retrieval` | No |
| 2026-02-06 | `Salesforce/Llama-Rank-V1` | No |
| 2026-02-06 | `togethercomputer/Refuel-Llm-V2` | No |
| 2026-02-06 | `togethercomputer/Refuel-Llm-V2-Small` | No |
| 2026-02-06 | `Qwen/Qwen3-235B-A22B-fp8-tput` | No |
| 2026-02-06 | `qwen-qwen2-5-14b-instruct-lora` | No |
| 2026-02-06 | `meta-llama/Llama-4-Scout-17B-16E-Instruct` | Yes |
| 2026-02-06 | `Qwen/Qwen2.5-72B-Instruct-Turbo` | No |
| 2026-02-06 | `meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo` | No |
| 2026-02-06 | `BAAI/bge-large-en-v1.5` | No |
| 2026-02-03 | `deepseek-ai/DeepSeek-R1-0528-tput` | No |
| 2026-01-05 | `Qwen/Qwen2.5-VL-72B-Instruct` | No |
| 2025-12-23 | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | No |
| 2025-12-23 | `meta-llama/Meta-Llama-3-70B-Instruct-Turbo` | No |
| 2025-12-23 | `black-forest-labs/FLUX.1-schnell-free` | No |
| 2025-12-23 | `meta-llama/Meta-Llama-Guard-3-8B` | No |
| 2025-11-19 | `deepcogito/cogito-v2-preview-deepseek-671b` | No |
| 2025-07-25 | `arcee-ai/caller` | No |
| 2025-07-25 | `arcee-ai/arcee-blitz` | No |
| 2025-07-25 | `arcee-ai/virtuoso-medium-v2` | No |
| 2025-11-17 | `arcee-ai/virtuoso-large` | No |
| 2025-11-17 | `arcee-ai/maestro-reasoning` | No |
| 2025-11-17 | `arcee_ai/arcee-spotlight` | No |
| 2025-11-17 | `arcee-ai/coder-large` | No |
| 2025-11-13 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-14B` | No |
| 2025-11-13 | `mistralai/Mistral-7B-Instruct-v0.1` | No |
| 2025-11-13 | `Qwen/Qwen2.5-Coder-32B-Instruct` | No |
| 2025-11-13 | `Qwen/QwQ-32B` | No |
| 2025-11-13 | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free` | No |
| 2025-11-13 | `meta-llama/Llama-3.3-70B-Instruct-Turbo-Free` | No |
| 2025-08-28 | `Qwen/Qwen2-VL-72B-Instruct` | No |
| 2025-08-28 | `nvidia/Llama-3.1-Nemotron-70B-Instruct-HF` | No |
| 2025-08-28 | `perplexity-ai/r1-1776` | No |
| 2025-08-28 | `meta-llama/Meta-Llama-3-8B-Instruct` | No |
| 2025-08-28 | `google/gemma-2-27b-it` | No |
| 2025-08-28 | `Qwen/Qwen2-72B-Instruct` | No |
| 2025-08-28 | `meta-llama/Llama-Vision-Free` | No |
| 2025-08-28 | `Qwen/Qwen2.5-14B` | No |
| 2025-08-28 | `meta-llama-llama-3-3-70b-instruct-lora` | No |
| 2025-08-28 | `meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo` | No |
| 2025-08-28 | `NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO` | No |
| 2025-08-28 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B` | No |
| 2025-08-28 | `black-forest-labs/FLUX.1-depth` | No |
| 2025-08-28 | `black-forest-labs/FLUX.1-redux` | No |
| 2025-08-28 | `meta-llama/Llama-3-8b-chat-hf` | No |
| 2025-08-28 | `black-forest-labs/FLUX.1-canny` | No |
| 2025-08-28 | `meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo` | No |
| 2025-06-13 | `gryphe-mythomax-l2-13b` | No |
| 2025-06-13 | `mistralai-mixtral-8x22b-instruct-v0-1` | No |
| 2025-06-13 | `mistralai-mixtral-8x7b-v0-1` | No |
| 2025-06-13 | `togethercomputer-m2-bert-80m-2k-retrieval` | No |
| 2025-06-13 | `togethercomputer-m2-bert-80m-8k-retrieval` | No |
| 2025-06-13 | `whereisai-uae-large-v1` | No |
| 2025-06-13 | `google-gemma-2-9b-it` | No |
| 2025-06-13 | `google-gemma-2b-it` | No |
| 2025-06-13 | `gryphe-mythomax-l2-13b-lite` | No |
| 2025-05-16 | `meta-llama-llama-3-2-3b-instruct-turbo-lora` | No |
| 2025-05-16 | `meta-llama-meta-llama-3-8b-instruct-turbo` | No |
| 2025-04-24 | `meta-llama/Llama-2-13b-chat-hf` | No |
| 2025-04-24 | `meta-llama-meta-llama-3-70b-instruct-turbo` | No |
| 2025-04-24 | `meta-llama-meta-llama-3-1-8b-instruct-turbo-lora` | No |
| 2025-04-24 | `meta-llama-meta-llama-3-1-70b-instruct-turbo-lora` | No |
| 2025-04-24 | `meta-llama-llama-3-2-1b-instruct-lora` | No |
| 2025-04-24 | `microsoft-wizardlm-2-8x22b` | No |
| 2025-04-24 | `upstage-solar-10-7b-instruct-v1` | No |
| 2025-04-14 | `stabilityai/stable-diffusion-xl-base-1.0` | No |
| 2025-04-04 | `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo-lora` | No |
| 2025-03-27 | `mistralai/Mistral-7B-v0.1` | No |
| 2025-03-25 | `Qwen/QwQ-32B-Preview` | No |
| 2025-03-13 | `databricks-dbrx-instruct` | No |
| 2025-03-11 | `meta-llama/Meta-Llama-3-70B-Instruct-Lite` | No |
| 2025-03-08 | `Meta-Llama/Llama-Guard-7b` | No |
| 2025-02-06 | `sentence-transformers/msmarco-bert-base-dot-v5` | No |
| 2025-02-06 | `bert-base-uncased` | No |
| 2024-10-29 | `Qwen/Qwen1.5-72B-Chat` | No |
| 2024-10-29 | `Qwen/Qwen1.5-110B-Chat` | No |
| 2024-10-07 | `NousResearch/Nous-Hermes-2-Yi-34B` | No |
| 2024-10-07 | `NousResearch/Hermes-3-Llama-3.1-405B-Turbo` | No |
| 2024-08-22 | `NousResearch/Nous-Hermes-2-Mistral-7B-DPO` | No |
| 2024-08-22 | `SG161222/Realistic_Vision_V3.0_VAE` | No |
| 2024-08-22 | `meta-llama/Llama-2-70b-chat-hf` | No |
| 2024-08-22 | `mistralai/Mixtral-8x22B` | No |
| 2024-08-22 | `Phind/Phind-CodeLlama-34B-v2` | No |
| 2024-08-22 | `meta-llama/Meta-Llama-3-70B` | No |
| 2024-08-22 | `teknium/OpenHermes-2p5-Mistral-7B` | No |
| 2024-08-22 | `openchat/openchat-3.5-1210` | No |
| 2024-08-22 | `WizardLM/WizardCoder-Python-34B-V1.0` | No |
| 2024-08-22 | `NousResearch/Nous-Hermes-2-Mixtral-8x7B-SFT` | No |
| 2024-08-22 | `NousResearch/Nous-Hermes-Llama2-13b` | No |
| 2024-08-22 | `zero-one-ai/Yi-34B-Chat` | No |
| 2024-08-22 | `codellama/CodeLlama-34b-Instruct-hf` | No |
| 2024-08-22 | `codellama/CodeLlama-34b-Python-hf` | No |
| 2024-08-22 | `teknium/OpenHermes-2-Mistral-7B` | No |
| 2024-08-22 | `Qwen/Qwen1.5-14B-Chat` | No |
| 2024-08-22 | `stabilityai/stable-diffusion-2-1` | No |
| 2024-08-22 | `meta-llama/Llama-3-8b-hf` | No |
| 2024-08-22 | `prompthero/openjourney` | No |
| 2024-08-22 | `runwayml/stable-diffusion-v1-5` | No |
| 2024-08-22 | `wavymulder/Analog-Diffusion` | No |
| 2024-08-22 | `Snowflake/snowflake-arctic-instruct` | No |
| 2024-08-22 | `deepseek-ai/deepseek-coder-33b-instruct` | No |
| 2024-08-22 | `Qwen/Qwen1.5-7B-Chat` | No |
| 2024-08-22 | `Qwen/Qwen1.5-32B-Chat` | No |
| 2024-08-22 | `cognitivecomputations/dolphin-2.5-mixtral-8x7b` | No |
| 2024-08-22 | `garage-bAInd/Platypus2-70B-instruct` | No |
| 2024-08-22 | `google/gemma-7b-it` | No |
| 2024-08-22 | `meta-llama/Llama-2-7b-chat-hf` | No |
| 2024-08-22 | `Qwen/Qwen1.5-32B` | No |
| 2024-08-22 | `Open-Orca/Mistral-7B-OpenOrca` | No |
| 2024-08-22 | `codellama/CodeLlama-13b-Instruct-hf` | No |
| 2024-08-22 | `NousResearch/Nous-Capybara-7B-V1p9` | No |
| 2024-08-22 | `lmsys/vicuna-13b-v1.5` | No |
| 2024-08-22 | `Undi95/ReMM-SLERP-L2-13B` | No |
| 2024-08-22 | `Undi95/Toppy-M-7B` | No |
| 2024-08-22 | `meta-llama/Llama-2-13b-hf` | No |
| 2024-08-22 | `codellama/CodeLlama-70b-Instruct-hf` | No |
| 2024-08-22 | `snorkelai/Snorkel-Mistral-PairRM-DPO` | No |
| 2024-08-22 | `togethercomputer/LLaMA-2-7B-32K-Instruct` | No |
| 2024-08-22 | `Austism/chronos-hermes-13b` | No |
| 2024-08-22 | `Qwen/Qwen1.5-72B` | No |
| 2024-08-22 | `zero-one-ai/Yi-34B` | No |
| 2024-08-22 | `codellama/CodeLlama-7b-Instruct-hf` | No |
| 2024-08-22 | `togethercomputer/evo-1-131k-base` | No |
| 2024-08-22 | `codellama/CodeLlama-70b-hf` | No |
| 2024-08-22 | `WizardLM/WizardLM-13B-V1.2` | No |
| 2024-08-22 | `meta-llama/Llama-2-7b-hf` | No |
| 2024-08-22 | `google/gemma-7b` | No |
| 2024-08-22 | `Qwen/Qwen1.5-1.8B-Chat` | No |
| 2024-08-22 | `Qwen/Qwen1.5-4B-Chat` | No |
| 2024-08-22 | `lmsys/vicuna-7b-v1.5` | No |
| 2024-08-22 | `zero-one-ai/Yi-6B` | No |
| 2024-08-22 | `Nexusflow/NexusRaven-V2-13B` | No |
| 2024-08-22 | `google/gemma-2b` | No |
| 2024-08-22 | `Qwen/Qwen1.5-7B` | No |
| 2024-08-22 | `NousResearch/Nous-Hermes-llama-2-7b` | No |
| 2024-08-22 | `togethercomputer/alpaca-7b` | No |
| 2024-08-22 | `Qwen/Qwen1.5-14B` | No |
| 2024-08-22 | `codellama/CodeLlama-70b-Python-hf` | No |
| 2024-08-22 | `Qwen/Qwen1.5-4B` | No |
| 2024-08-22 | `togethercomputer/StripedHyena-Hessian-7B` | No |
| 2024-08-22 | `allenai/OLMo-7B-Instruct` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-7B-Instruct` | No |
| 2024-08-22 | `togethercomputer/LLaMA-2-7B-32K` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-7B-Base` | No |
| 2024-08-22 | `Qwen/Qwen1.5-0.5B-Chat` | No |
| 2024-08-22 | `microsoft/phi-2` | No |
| 2024-08-22 | `Qwen/Qwen1.5-0.5B` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-7B-Chat` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-Chat-3B-v1` | No |
| 2024-08-22 | `togethercomputer/GPT-JT-Moderation-6B` | No |
| 2024-08-22 | `Qwen/Qwen1.5-1.8B` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-Instruct-3B-v1` | No |
| 2024-08-22 | `togethercomputer/RedPajama-INCITE-Base-3B-v1` | No |
| 2024-08-22 | `WhereIsAI/UAE-Large-V1` | No |
| 2024-08-22 | `allenai/OLMo-7B` | No |
| 2024-08-22 | `togethercomputer/evo-1-8k-base` | No |
| 2024-08-22 | `WizardLM/WizardCoder-15B-V1.0` | No |
| 2024-08-22 | `codellama/CodeLlama-13b-Python-hf` | No |
| 2024-08-22 | `allenai-olmo-7b-twin-2t` | No |
| 2024-08-22 | `sentence-transformers/msmarco-bert-base-dot-v5` | No |
| 2024-08-22 | `codellama/CodeLlama-7b-Python-hf` | No |
| 2024-08-22 | `hazyresearch/M2-BERT-2k-Retrieval-Encoder-V1` | No |
| 2024-08-22 | `bert-base-uncased` | No |
| 2024-08-22 | `mistralai/Mistral-7B-Instruct-v0.1-json` | No |
| 2024-08-22 | `mistralai/Mistral-7B-Instruct-v0.1-tools` | No |
| 2024-08-22 | `togethercomputer-codellama-34b-instruct-json` | No |
| 2024-08-22 | `togethercomputer-codellama-34b-instruct-tools` | No |
| **Notes on model support:** | | |
* The support column reflects the current [supported models](/docs/dedicated-endpoints/models) catalog for dedicated model inference and is updated automatically as the catalog changes.
* Models marked "Yes" can be deployed as on-demand dedicated endpoints, either under the listed ID or as the underlying base model of a serving variant (for example, a deprecated `-FP8` or `-Turbo` ID).
* Models marked "No" are not available as on-demand endpoints and require migration to a different model or a monthly reserved dedicated endpoint.
### Fine-tuning
The table below lists all models removed from the fine-tuning service, most recent first. These models can no longer be used as a base model for a fine-tuning job. Where a close equivalent exists, the suggested replacement is listed. A blank cell means there is no direct equivalent. See [Supported models](/docs/fine-tuning/supported-models) for the full list of models available today.
| Removal date | Model | Suggested replacement |
| :----------- | :------------------------------------------------------ | :------------------------------------------------ |
| 2026-07-29 | `nvidia/NVIDIA-Nemotron-Nano-9B-v2` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen3-Next-80B-A3B-Instruct` | `Qwen/Qwen3.5-122B-A10B` |
| 2026-07-29 | `Qwen/Qwen3-Next-80B-A3B-Thinking` | `Qwen/Qwen3.5-122B-A10B` |
| 2026-07-29 | `Qwen/Qwen3-0.6B` | `Qwen/Qwen3.5-0.8B` |
| 2026-07-29 | `Qwen/Qwen3-0.6B-Base` | `Qwen/Qwen3.5-0.8B` |
| 2026-07-29 | `Qwen/Qwen3-1.7B` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `Qwen/Qwen3-1.7B-Base` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `Qwen/Qwen3-4B` | `Qwen/Qwen3.5-4B` |
| 2026-07-29 | `Qwen/Qwen3-4B-Base` | `Qwen/Qwen3.5-4B` |
| 2026-07-29 | `Qwen/Qwen3-8B` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen3-8B-Base` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen3-14B` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen3-14B-Base` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen3-32B` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen3-30B-A3B-Base` | `Qwen/Qwen3.6-35B-A3B` |
| 2026-07-29 | `Qwen/Qwen3-30B-A3B` | `Qwen/Qwen3.6-35B-A3B` |
| 2026-07-29 | `Qwen/Qwen3-30B-A3B-Instruct-2507` | `Qwen/Qwen3.6-35B-A3B` |
| 2026-07-29 | `Qwen/Qwen3-235B-A22B` | `Qwen/Qwen3.5-397B-A17B` |
| 2026-07-29 | `Qwen/Qwen3-235B-A22B-Instruct-2507` | `Qwen/Qwen3.5-397B-A17B` |
| 2026-07-29 | `Qwen/Qwen3-Coder-30B-A3B-Instruct` | `Qwen/Qwen3.6-35B-A3B` |
| 2026-07-29 | `Qwen/Qwen3-Coder-480B-A35B-Instruct` | |
| 2026-07-29 | `Qwen/Qwen3-VL-8B-Instruct` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen3-VL-32B-Instruct` | |
| 2026-07-29 | `Qwen/Qwen3-VL-30B-A3B-Instruct` | `Qwen/Qwen3.5-4B` |
| 2026-07-29 | `Qwen/Qwen3-VL-235B-A22B-Instruct` | |
| 2026-07-29 | `Qwen/Qwen2.5-72B-Instruct` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `Qwen/Qwen2.5-72B` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `Qwen/Qwen2.5-32B-Instruct` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen2.5-32B` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen2.5-14B-Instruct` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen2.5-14B` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `Qwen/Qwen2.5-7B-Instruct` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen2.5-7B` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen2.5-3B-Instruct` | `Qwen/Qwen3.5-4B` |
| 2026-07-29 | `Qwen/Qwen2.5-3B` | `Qwen/Qwen3.5-4B` |
| 2026-07-29 | `Qwen/Qwen2.5-1.5B-Instruct` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `Qwen/Qwen2.5-1.5B` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `Qwen/Qwen2-72B-Instruct` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `Qwen/Qwen2-72B` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `Qwen/Qwen2-7B-Instruct` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen2-7B` | `Qwen/Qwen3.5-9B` |
| 2026-07-29 | `Qwen/Qwen2-1.5B-Instruct` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `Qwen/Qwen2-1.5B` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `moonshotai/Kimi-K2.5` | `moonshotai/Kimi-K2.6` |
| 2026-07-29 | `moonshotai/Kimi-K2-Thinking` | `moonshotai/Kimi-K2.6` |
| 2026-07-29 | `moonshotai/Kimi-K2-Instruct-0905` | `moonshotai/Kimi-K2.6` |
| 2026-07-29 | `moonshotai/Kimi-K2-Instruct` | `moonshotai/Kimi-K2.6` |
| 2026-07-29 | `moonshotai/Kimi-K2-Base` | `moonshotai/Kimi-K2.6` |
| 2026-07-29 | `zai-org/GLM-5` | `zai-org/GLM-5.1` |
| 2026-07-29 | `zai-org/GLM-4.7` | `zai-org/GLM-5.1` |
| 2026-07-29 | `zai-org/GLM-4.6` | `zai-org/GLM-5.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-0528` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-V3-0324` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-V3` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-V3.1-Base` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-V3-Base` | `deepseek-ai/DeepSeek-V3.1` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B-32k` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-Distill-Llama-70B-131k` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-14B` | `Qwen/Qwen3.5-27B` |
| 2026-07-29 | `deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B` | `Qwen/Qwen3.5-2B` |
| 2026-07-29 | `meta-llama/Llama-4-Scout-17B-16E` | `meta-llama/Llama-4-Scout-17B-16E-Instruct` |
| 2026-07-29 | `meta-llama/Llama-4-Maverick-17B-128E` | `meta-llama/Llama-4-Maverick-17B-128E-Instruct` |
| 2026-07-29 | `meta-llama/Llama-3.3-70B-32k-Instruct-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Llama-3.3-70B-131k-Instruct-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Llama-3.2-3B-Instruct` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Llama-3.2-3B` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Llama-3.2-1B-Instruct` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Llama-3.2-1B` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-8B-131k-Instruct-Reference` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-8B-Reference` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-8B-131k-Reference` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-Instruct-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-32k-Instruct-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-131k-Instruct-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-32k-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-70B-131k-Reference` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-Instruct-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-10k-Instruct-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-10k-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-8k-Instruct-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3.1-405B-8k-Reference` | |
| 2026-07-29 | `meta-llama/Meta-Llama-3-8B-Instruct` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3-8B` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
| 2026-07-29 | `meta-llama/Meta-Llama-3-70B-Instruct` | `meta-llama/Llama-3.3-70B-Instruct-Reference` |
| 2026-07-29 | `google/gemma-3-270m` | `Qwen/Qwen3.5-0.8B` |
| 2026-07-29 | `google/gemma-3-270m-it` | `Qwen/Qwen3.5-0.8B` |
| 2026-07-29 | `google/gemma-3-1b-it` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-1b-pt` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-4b-it` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-4b-it-VLM` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-4b-pt` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-12b-it` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-12b-it-VLM` | `google/gemma-4-31B-it-VLM` |
| 2026-07-29 | `google/gemma-3-12b-pt` | `google/gemma-4-26B-A4B-it` |
| 2026-07-29 | `google/gemma-3-27b-it` | `google/gemma-4-31B-it` |
| 2026-07-29 | `google/gemma-3-27b-it-VLM` | `google/gemma-4-31B-it-VLM` |
| 2026-07-29 | `google/gemma-3-27b-pt` | `google/gemma-4-31B-it` |
| 2026-07-29 | `mistralai/Mixtral-8x7B-v0.1` | `mistralai/Mixtral-8x7B-Instruct-v0.1` |
| 2026-07-29 | `mistralai/Mistral-7B-Instruct-v0.2` | `mistralai/Mixtral-8x7B-Instruct-v0.1` |
| 2026-07-29 | `mistralai/Mistral-7B-v0.1` | `mistralai/Mixtral-8x7B-Instruct-v0.1` |
| 2026-07-29 | `togethercomputer/llama-2-7b-chat` | `meta-llama/Meta-Llama-3.1-8B-Instruct-Reference` |
## Recommended actions
* Regularly check this page for updates on model deprecations.
* Plan your migration well in advance of the removal date to ensure a smooth transition.
* If you have any questions or need assistance with migration, contact the Together AI support team.
For the most up-to-date information on model availability, support, and recommended alternatives, check the API documentation or contact the Together AI support team.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,351 @@
import importlib.util
import json
from pathlib import Path
from types import MappingProxyType
import pytest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "sync_together_ai_models.py"
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "together_ai_sync"
_spec = importlib.util.spec_from_file_location("sync_together_ai_models", SCRIPT)
assert _spec is not None and _spec.loader is not None
sync = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(sync)
RECORDED_CATALOG = sync.load_catalog(FIXTURES.joinpath("models_serverless.json").read_bytes())
RECORDED_DOC = sync.parse_deprecations(FIXTURES.joinpath("deprecations.md").read_text())
def _doc(removal_dates: dict[str, str], redirects: dict[str, str] | None = None) -> object:
return sync.DeprecationDoc(
removal_dates=MappingProxyType(removal_dates),
redirects=MappingProxyType(redirects or {}),
)
def _chat_model(model_id: str, ctx: int = 4096, price: float = 1.0, cached: float | None = None) -> object:
return sync.CatalogModel(
id=model_id,
type="chat",
context_length=ctx,
pricing=sync.CatalogPricing(input=price, output=price, cached_input=cached),
)
@pytest.mark.parametrize(
("per_million", "expected"),
[
(3, 3e-06),
(15, 1.5e-05),
(1.4, 1.4e-06),
(0.25999999999999995, 2.6e-07),
(0.060000000000000005, 6e-08),
(1.0399999999999998, 1.04e-06),
(0, 0.0),
],
)
def test_per_token_normalizes_float_artifacts(per_million: float, expected: float) -> None:
assert sync.per_token(per_million) == expected
def test_parse_deprecations_recorded_fixture() -> None:
assert dict(RECORDED_DOC.redirects) == {
"mistralai/Mistral-7B-Instruct-v0.3": "mistralai/Ministral-3-14B-Instruct-2512",
"Kimi-K2": "Kimi-K2-0905",
"DeepSeek-V3": "DeepSeek-V3.1",
"DeepSeek-V3-0324": "DeepSeek-V3.1",
"DeepSeek-R1": "DeepSeek-R1-0528",
}
assert len(RECORDED_DOC.removal_dates) == 208
assert RECORDED_DOC.removal_dates["google/gemma-3n-E4B-it"] == "2026-08-04"
def test_parse_deprecations_duplicate_rows_keep_most_recent_date() -> None:
assert RECORDED_DOC.removal_dates["Qwen/Qwen3-235B-A22B-Thinking-2507"] == "2026-04-16"
@pytest.mark.parametrize(
"markdown",
[
"# Deprecations\n\nNothing here anymore.\n",
"\n## Active model redirects\n\n| A | B |\n| --- | --- |\n| `x` | `y` |\n\n## Something else\n",
"\n## Deprecation history\n\n### Inference\n\n| Date | Model | R |\n| --- | --- | --- |\n| 2026-01-01 | `m` | No |\n",
],
)
def test_parse_deprecations_raises_when_a_table_parses_empty(markdown: str) -> None:
with pytest.raises(sync.SyncError):
sync.parse_deprecations(markdown)
def test_load_catalog_raises_on_shape_change() -> None:
with pytest.raises(sync.SyncError):
sync.load_catalog(b'[{"id": "x", "type": "chat"}]')
def test_load_catalog_raises_when_no_token_models_remain() -> None:
only_video = json.dumps([{"id": "v", "type": "video", "pricing": {"input": 0, "output": 0}}]).encode()
with pytest.raises(sync.SyncError):
sync.load_catalog(only_video)
def test_recorded_catalog_counts() -> None:
assert len(RECORDED_CATALOG) == 102
assert sum(1 for model in RECORDED_CATALOG if model.type in sync.TYPE_TO_MODE) == 26
assert sum(1 for model in RECORDED_CATALOG if model.pricing.cached_input) == 13
def test_added_chat_model_matches_reviewed_registry_shape() -> None:
outcome = sync.compute_sync({}, RECORDED_CATALOG, RECORDED_DOC)
assert len(outcome.added) == 26
assert not outcome.deprecated
assert outcome.cost_map["together_ai/moonshotai/Kimi-K3"] == {
"cache_read_input_token_cost": 3e-07,
"input_cost_per_token": 3e-06,
"litellm_provider": "together_ai",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
"source": "https://docs.together.ai/docs/serverless-models",
"supports_function_calling": True,
"supports_parallel_function_calling": True,
"supports_prompt_caching": True,
"supports_reasoning": True,
"supports_response_schema": True,
"supports_tool_choice": True,
"supports_vision": True,
}
def test_added_embedding_model_has_no_output_token_cap() -> None:
outcome = sync.compute_sync({}, RECORDED_CATALOG, RECORDED_DOC)
assert outcome.cost_map["together_ai/intfloat/multilingual-e5-large-instruct"] == {
"input_cost_per_token": 2e-08,
"litellm_provider": "together_ai",
"max_input_tokens": 514,
"max_tokens": 514,
"mode": "embedding",
"output_cost_per_token": 2e-08,
"output_vector_size": 1024,
"source": "https://docs.together.ai/docs/serverless-models",
}
def test_moderation_type_maps_to_chat_mode() -> None:
outcome = sync.compute_sync({}, RECORDED_CATALOG, RECORDED_DOC)
guard = outcome.cost_map["together_ai/meta-llama/Llama-Guard-4-12B"]
assert guard["mode"] == "chat"
assert guard["max_output_tokens"] == 1048576
def test_docs_removed_but_live_model_stays_live_with_warning() -> None:
outcome = sync.compute_sync({}, RECORDED_CATALOG, RECORDED_DOC)
gemma = outcome.cost_map["together_ai/google/gemma-3n-E4B-it"]
assert "deprecation_date" not in gemma
assert any("gemma-3n-E4B-it" in warning and "2026-08-04" in warning for warning in outcome.warnings)
def test_price_change_updates_api_fields_and_keeps_curated_ones() -> None:
registry = {
"together_ai/acme/chat-1": {
"input_cost_per_token": 9e-07,
"litellm_provider": "together_ai",
"max_input_tokens": 4096,
"max_output_tokens": 2048,
"max_tokens": 2048,
"mode": "chat",
"output_cost_per_token": 9e-07,
"supports_audio_input": True,
}
}
outcome = sync.compute_sync(registry, [_chat_model("acme/chat-1", ctx=8192, price=2.0)], _doc({"x": "2026-01-01"}))
entry = outcome.cost_map["together_ai/acme/chat-1"]
assert entry["input_cost_per_token"] == 2e-06
assert entry["max_input_tokens"] == 8192
assert entry["max_output_tokens"] == 2048
assert entry["supports_audio_input"] is True
assert len(outcome.updated) == 1
assert "input_cost_per_token" in outcome.updated[0]
def test_cached_input_appearing_and_disappearing() -> None:
registry = {
"together_ai/acme/chat-1": {
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"max_input_tokens": 4096,
"mode": "chat",
"output_cost_per_token": 1e-06,
"supports_prompt_caching": True,
},
"together_ai/acme/chat-2": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"max_input_tokens": 4096,
"mode": "chat",
"output_cost_per_token": 1e-06,
},
}
catalog = [_chat_model("acme/chat-1"), _chat_model("acme/chat-2", cached=0.25999999999999995)]
outcome = sync.compute_sync(registry, catalog, _doc({"x": "2026-01-01"}))
assert "cache_read_input_token_cost" not in outcome.cost_map["together_ai/acme/chat-1"]
assert "supports_prompt_caching" not in outcome.cost_map["together_ai/acme/chat-1"]
assert outcome.cost_map["together_ai/acme/chat-2"]["cache_read_input_token_cost"] == 2.6e-07
assert outcome.cost_map["together_ai/acme/chat-2"]["supports_prompt_caching"] is True
def test_capability_rule_backfills_existing_entry() -> None:
registry = {
"together_ai/moonshotai/Kimi-K3": {
"input_cost_per_token": 3e-06,
"litellm_provider": "together_ai",
"max_input_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.5e-05,
}
}
kimi = next(model for model in RECORDED_CATALOG if model.id == "moonshotai/Kimi-K3")
outcome = sync.compute_sync(registry, [kimi], _doc({"x": "2026-01-01"}))
assert outcome.cost_map["together_ai/moonshotai/Kimi-K3"]["supports_reasoning"] is True
assert any("supports_reasoning" in line for line in outcome.updated)
def test_disappeared_model_gets_docs_date_and_is_never_deleted() -> None:
registry = {
"together_ai/acme/gone": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"mode": "chat",
"output_cost_per_token": 1e-06,
}
}
outcome = sync.compute_sync(registry, [_chat_model("acme/alive")], _doc({"acme/gone": "2026-07-01"}))
assert outcome.cost_map["together_ai/acme/gone"]["deprecation_date"] == "2026-07-01"
assert outcome.deprecated == ("together_ai/acme/gone: deprecation_date",)
def test_disappeared_model_without_docs_date_warns_instead() -> None:
registry = {
"together_ai/acme/gone": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"mode": "chat",
"output_cost_per_token": 1e-06,
}
}
outcome = sync.compute_sync(registry, [_chat_model("acme/alive")], _doc({"other": "2026-07-01"}))
assert "deprecation_date" not in outcome.cost_map["together_ai/acme/gone"]
assert not outcome.deprecated
assert any("acme/gone" in warning and "human" in warning for warning in outcome.warnings)
def test_curated_deprecation_date_is_never_overwritten() -> None:
registry = {
"together_ai/acme/gone": {
"deprecation_date": "2026-06-15",
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"mode": "chat",
"output_cost_per_token": 1e-06,
}
}
outcome = sync.compute_sync(registry, [_chat_model("acme/alive")], _doc({"acme/gone": "2026-07-01"}))
assert outcome.cost_map["together_ai/acme/gone"]["deprecation_date"] == "2026-06-15"
assert any("2026-06-15" in warning and "2026-07-01" in warning for warning in outcome.warnings)
def test_redirect_chain_resolves_to_final_live_model() -> None:
doc = _doc({"acme/a": "2026-01-01"}, redirects={"acme/a": "acme/b", "acme/b": "acme/c"})
live = frozenset({"acme/c"})
assert sync.resolve_successor("acme/a", doc, live) == "acme/c"
def test_redirect_dead_end_yields_no_successor() -> None:
doc = _doc({"acme/a": "2026-01-01"}, redirects={"acme/a": "acme/b"})
assert sync.resolve_successor("acme/a", doc, frozenset({"acme/other"})) is None
def test_redirect_short_names_resolve_by_unique_suffix() -> None:
doc = _doc({"moonshotai/Kimi-K2": "2026-01-01"}, redirects={"Kimi-K2": "Kimi-K2-0905"})
live = frozenset({"moonshotai/Kimi-K2-0905"})
assert sync.resolve_successor("moonshotai/Kimi-K2", doc, live) == "moonshotai/Kimi-K2-0905"
def test_successor_written_only_when_not_curated() -> None:
registry = {
"together_ai/acme/a": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"mode": "chat",
"output_cost_per_token": 1e-06,
},
"together_ai/acme/b": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"metadata": {"successor": "together_ai/acme/curated"},
"mode": "chat",
"output_cost_per_token": 1e-06,
},
}
doc = _doc({"acme/a": "2026-01-01", "acme/b": "2026-01-01"}, redirects={"acme/a": "acme/c", "acme/b": "acme/c"})
outcome = sync.compute_sync(registry, [_chat_model("acme/c")], doc)
assert outcome.cost_map["together_ai/acme/a"]["metadata"] == {"successor": "together_ai/acme/c"}
assert outcome.cost_map["together_ai/acme/b"]["metadata"] == {"successor": "together_ai/acme/curated"}
assert any("acme/curated" in warning for warning in outcome.warnings)
def test_reappearance_clears_deprecation_date() -> None:
registry = {
"together_ai/acme/back": {
"deprecation_date": "2026-05-01",
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"max_input_tokens": 4096,
"mode": "chat",
"output_cost_per_token": 1e-06,
}
}
outcome = sync.compute_sync(registry, [_chat_model("acme/back")], _doc({"x": "2026-01-01"}))
assert "deprecation_date" not in outcome.cost_map["together_ai/acme/back"]
assert outcome.reappeared == ("together_ai/acme/back",)
def test_new_chat_model_without_rule_is_flagged() -> None:
outcome = sync.compute_sync({}, [_chat_model("acme/unreviewed")], _doc({"x": "2026-01-01"}))
assert any("acme/unreviewed" in warning and "capability rule" in warning for warning in outcome.warnings)
def test_new_keys_land_at_the_end_of_the_provider_block() -> None:
registry = {
"aaa": {"mode": "chat"},
"together_ai/acme/old": {
"input_cost_per_token": 1e-06,
"litellm_provider": "together_ai",
"mode": "chat",
"output_cost_per_token": 1e-06,
},
"zzz": {"mode": "chat"},
}
outcome = sync.compute_sync(registry, [_chat_model("acme/old"), _chat_model("acme/new")], _doc({"x": "2026-01-01"}))
assert list(outcome.cost_map) == ["aaa", "together_ai/acme/old", "together_ai/acme/new", "zzz"]
def test_sync_is_idempotent_over_the_repo_cost_map() -> None:
cost_map = json.loads((ROOT / "model_prices_and_context_window.json").read_text())
first = sync.compute_sync(cost_map, RECORDED_CATALOG, RECORDED_DOC)
second = sync.compute_sync(first.cost_map, RECORDED_CATALOG, RECORDED_DOC)
assert not second.has_changes
assert second.cost_map == first.cost_map
def test_pr_body_lists_every_section_and_the_skipped_types() -> None:
outcome = sync.compute_sync({}, RECORDED_CATALOG, RECORDED_DOC)
body = sync.render_pr_body(outcome)
assert "### Added (26)" in body
assert "### Warnings needing a human call" in body
assert "image (29)" in body
assert "video (38)" in body