feat(ai): refactor provider definitions (#11368)

This builds the model catalog from per-provider definitions and
models.dev v2 data.

It eplacer `generate-models.ts` with one `defineProvider()` file per
built-in provider, shared model-family rules (Claude, GPT, DeepSeek,
Kimi, Google), and source adapters. models.dev is read through its v2
API. Outputs and CLI flags are unchanged.
This commit is contained in:
Armin Ronacher 2026-10-28 11:06:10 +01:00 • committed by GitHub
parent 068e972c7b
commit 2e0f175bd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
120 changed files with 3952 additions and 3899 deletions

View file

@ -23,11 +23,11 @@ Most providers reuse an existing API such as `openai-completions`; skip this ste
- Register the lazy wrapper in the builtin API list in `src/compat.ts`.
- Add `export type` re-exports in `src/index.ts` for option types that should be available from the root entry. The `./api/*` subpath export in `package.json` already covers the new module.
## 3. Model Generation (`packages/ai/scripts/generate-models.ts`)
## 3. Model Catalog (`packages/ai/catalog/providers/<id>.ts`)
- Add logic to fetch/parse models from the provider source.
- Map to the standardized `Model` interface.
- Regenerate. This writes the stable catalog wrapper `src/providers/<id>.models.ts` (exporting `<ID>_MODELS`) that the provider factory imports, and updates `src/models.generated.ts`.
- Add a provider definition with `defineProvider()` and register it in `catalog/providers/index.ts`. Read `packages/ai/catalog/README.md` first.
- For a models.dev provider, use `modelsDevModels()`; set compat defaults per API in `compat` and provider-specific behavior in `finalize`.
- Regenerate with `npm run generate-models` in `packages/ai`. This writes the stable catalog wrapper `src/providers/<id>.models.ts` (exporting `<ID>_MODELS`) that the provider factory imports, and updates `src/models.generated.ts`.
## 4. Provider Factory and Auth (`packages/ai/src/providers/`)

View file

@ -25,7 +25,7 @@
- Always ask before removing functionality or code that appears intentional.
- Do not preserve backward compatibility unless the user asks for it.
- Never hardcode key checks (e.g. `matchesKey(keyData, "ctrl+x")`). Add defaults to `DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS` so they stay configurable.
- Never modify `packages/ai/src/models.generated.ts` directly; update `packages/ai/scripts/generate-models.ts` instead, then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.
- Never modify `packages/ai/src/models.generated.ts` directly; update the catalog definitions in `packages/ai/catalog/` instead (see its README), then regenerate. Including the resulting `models.generated.ts` diff is always OK, even if regeneration includes unrelated upstream model metadata changes. The generated model data in `packages/ai/src/providers/data/` is gitignored: never stage it, and don't look for it in git history.
## Commands

View file

@ -29,6 +29,7 @@
"scripts/model-catalog-protocol.test.ts",
"packages/*/src/**/*.ts",
"packages/*/test/**/*.ts",
"packages/ai/catalog/**/*.ts",
"packages/coding-agent/examples/**/*.ts",
"packages/agent/examples/**/*.ts",
"!**/node_modules/**/*",

View file

@ -15,6 +15,7 @@
### Changed
- The faux provider streams partial tool-call arguments, so `toolcall_delta` partials show growing arguments as real providers do ([#9265](https://github.com/earendil-works/pi/issues/9265))
- The built-in model catalog is generated from models.dev's v2 catalog, and its per-provider rules moved from `scripts/generate-models.ts` into one TypeScript definition per provider under `catalog/`. Models that models.dev types as realtime or image models (`gpt-realtime-2.1`, `gemini-3.1-flash-live-preview`, `gemini-3.1-flash-lite-image`) are no longer offered as chat models, and models.dev's newer OpenAI and Google chat models (`chat-latest`, `gpt-5.6-cyber`, `gemini-robotics-er-2-preview`) are now included
### Fixed

View file

@ -1928,12 +1928,12 @@ Create a new API implementation file (for example `bedrock-converse-stream.ts`)
Add a lazy wrapper `src/api/<api-id>.lazy.ts` (`<name>Api()` via `lazyApi()`) so providers can reference the implementation without importing its SDK. Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai`.
#### 3. Model Generation (`scripts/generate-models.ts`)
#### 3. Model Catalog (`catalog/providers/<id>.ts`)
- Add logic to fetch and parse models from the provider's source (e.g., models.dev API)
- Add a provider definition and register it in `catalog/providers/index.ts`; see `catalog/README.md` for the build order and helpers
- Map chat/tool-capable provider data to `Model`, image-generation data to `ImageModel`, and models.dev `type: "decision"` entries to `ClassifierModel`; hydration groups the ignored `src/providers/data/<id>.json` values by API while stable `src/providers/<id>.models.ts` wrappers derive exact model/API types directly from those JSON keys
- Keep model ids unique within each provider and model type; emit separate entries when an upstream model supports multiple operations
- Handle provider-specific quirks (pricing format, capability flags, model ID transformations)
- Handle provider-specific quirks (pricing format, capability flags, model ID transformations) in the provider definition
#### 4. Provider Factory (`src/providers/<id>.ts`)

View file

@ -0,0 +1,47 @@
# Model catalog
`node catalog/generate.ts` (`npm run generate-models`) fetches upstream catalogs and writes the built-in model catalog:
- `src/providers/data/<provider>.json`: model values, grouped by API (gitignored)
- `src/providers/<provider>.models.ts` and `src/models.generated.ts`: typed wrappers (committed)
- with `--json-output <dir>`: the public catalog JSON served by pi.dev
## Layout
| Path | Contents |
| --- | --- |
| `sources/` | Upstream fetchers. `models-dev.ts` normalizes the [models.dev v2](https://models.dev/experimental/v2.0/api.json) catalog; OpenRouter, Vercel AI Gateway, NVIDIA NIM, and Radius have their own modules. All sources are fetched before anything is built. |
| `providers/<id>.ts` | One file per built-in provider: where its models come from, its API and base URL, compat defaults, corrections, and models upstream does not list. Register new files in `providers/index.ts`. |
| `families/` | Facts about a model family that hold on every host, such as Claude effort levels or GPT-6 thinking levels. They apply to every provider's models. |
| `define.ts` | `defineProvider()` and the helpers provider files use. |
| `build.ts` | Runs the provider definitions in a fixed order. |
## Build order
For each provider:
1. `models(ctx)` returns its chat models: upstream entries mapped to pi models, data corrections (limits, prices), and authored models. `ctx.providerModels(id)` returns another provider's models from this step, e.g. Azure derives from OpenAI.
2. `compat` defaults for each API are merged under each model's own compat. Then the shared rules run: models.dev effort levels for APIs that send them natively, and the rules in `families/`.
3. `finalize(model, ctx)` applies provider-specific behavior. It runs after the shared rules, so its values win.
4. Image input limits get the default resize profile, then `finalizeAll(models, ctx)` runs cross-model logic such as Anthropic's allowed fallback models.
## Adding a provider
Most providers are a models.dev provider served on one API:
```ts
import { defineProvider, modelsDevModels } from "../define.ts";
export default defineProvider({
id: "example",
models: (ctx) =>
modelsDevModels(ctx, "example", { api: "openai-completions", baseUrl: "https://api.example.com/v1" }),
compat: { "openai-completions": { supportsStore: false, supportsDeveloperRole: false } },
});
```
`modelsDevModels()` keeps tool-capable chat models and maps names, input modalities, costs (including context tiers), and limits. Use its `include` and `map` options for provider-specific filtering and fields. Prefer fixing wrong upstream data in models.dev; keep local corrections small and say why they exist.
## Checking changes
`npm run diff:model-catalog -- [provider ...]` from the repository root generates the catalog at `HEAD` and in the worktree and prints the JSON differences.

View file

@ -0,0 +1,164 @@
import type {
AnyModel,
Api,
ClassifierApi,
ClassifierModel,
ImageApi,
ImageModel,
KnownApi,
Model,
OpenAICompletionsCompat,
} from "../src/types.ts";
import type { ApiCompatDefaults, ChatModel, ProviderContext, ProviderDefinition } from "./define.ts";
import { mergeThinking } from "./define.ts";
import { applyClaudeFamilyRules } from "./families/anthropic.ts";
import { applyDeepSeekCompatRules, applyDeepSeekFamilyRules } from "./families/deepseek.ts";
import { applyOpenAiFamilyRules } from "./families/openai.ts";
import { getEffortThinkingLevelMap } from "./reasoning.ts";
import type { CatalogSources } from "./sources/index.ts";
import type { ModelsDevModel } from "./sources/models-dev.ts";
export interface ProviderCatalog {
chat: (Model<Api> & { type: "chat" })[];
image: ImageModel<ImageApi>[];
classifier: ClassifierModel<ClassifierApi>[];
}
// Keep the generated default no less restrictive than coding-agent's historical
// image preprocessing. Provider limits can narrow this profile, but unknown
// providers retain the cache-safe 2000px / 4.5 MiB behavior.
const DEFAULT_IMAGE_RESIZE = {
maxWidth: 2000,
maxHeight: 2000,
maxBytes: 4.5 * 1024 * 1024,
jpegQuality: 80,
} as const;
function modelKey(model: Pick<AnyModel, "provider" | "id">): string {
return `${model.provider}:${model.id}`;
}
function applyCompatDefaults(model: ChatModel, defaults: ApiCompatDefaults | undefined): void {
const entry = defaults?.[model.api as KnownApi] as
| Model<Api>["compat"]
| ((model: Model<Api>) => Model<Api>["compat"])
| undefined;
if (!entry) return;
const compat = typeof entry === "function" ? entry(model) : entry;
model.compat = { ...compat, ...model.compat } as Model<Api>["compat"];
}
/** Whether the model's API sends models.dev effort values to the host as-is. */
function sendsNativeEffort(model: ChatModel): boolean {
if (model.api === "anthropic-messages") {
return (model as Model<"anthropic-messages">).compat?.forceAdaptiveThinking === true;
}
if (
model.api === "openai-responses" ||
model.api === "azure-openai-responses" ||
model.api === "openai-codex-responses"
) {
return true;
}
if (model.api !== "openai-completions") return false;
const compat: OpenAICompletionsCompat = (model as Model<"openai-completions">).compat ?? {};
return (compat.thinkingFormat ?? "openai") === "openai" && (compat.supportsReasoningEffort ?? true);
}
function applyModelsDevEffort(model: ChatModel, source: ModelsDevModel | undefined): void {
if (!source?.reasoningOptions || !sendsNativeEffort(model)) return;
const map = getEffortThinkingLevelMap(source.reasoningOptions);
if (map) mergeThinking(model, map);
}
function applyImageResizeDefaults(model: AnyModel): void {
if (!model.input.includes("image")) return;
const images = model.inputLimits?.images;
model.inputLimits = {
...model.inputLimits,
images: { ...images, resize: { ...DEFAULT_IMAGE_RESIZE, ...images?.resize } },
};
}
/** Build every provider's catalog from the fetched sources. */
export function buildCatalog(
definitions: readonly ProviderDefinition[],
sources: CatalogSources,
strict: boolean,
): Record<string, ProviderCatalog> {
const definitionsById = new Map(definitions.map((definition) => [definition.id, definition]));
const modelsDevSources = new Map<string, ModelsDevModel>();
const stageOne = new Map<string, ChatModel[]>();
const building = new Set<string>();
function context(providerId: string): ProviderContext {
return {
providerId,
sources,
strict,
providerModels: (id) => structuredClone(buildModels(id)),
modelsDevSource: (model) => modelsDevSources.get(modelKey(model)),
recordModelsDevSource: (model, source) => {
modelsDevSources.set(modelKey(model), source);
},
};
}
function buildModels(providerId: string): ChatModel[] {
const built = stageOne.get(providerId);
if (built) return built;
const definition = definitionsById.get(providerId);
if (!definition) throw new Error(`Unknown provider: ${providerId}`);
if (building.has(providerId)) throw new Error(`Provider ${providerId} depends on itself`);
building.add(providerId);
const models = definition.models?.(context(providerId)) ?? [];
building.delete(providerId);
for (const model of models) {
if (model.provider !== providerId) {
throw new Error(`${providerId} produced ${modelKey(model)} for another provider`);
}
}
stageOne.set(providerId, models);
return models;
}
for (const definition of definitions) buildModels(definition.id);
const catalogs: Record<string, ProviderCatalog> = {};
for (const definition of definitions) {
const ctx = context(definition.id);
const chat: ChatModel[] = [];
const seen = new Set<string>();
for (const model of stageOne.get(definition.id) ?? []) {
// The first source listing a model wins.
if (seen.has(model.id)) continue;
seen.add(model.id);
chat.push(model);
}
for (const model of chat) {
applyCompatDefaults(model, definition.compat);
applyDeepSeekCompatRules(model);
applyModelsDevEffort(model, ctx.modelsDevSource(model));
applyOpenAiFamilyRules(model);
applyClaudeFamilyRules(model);
applyDeepSeekFamilyRules(model);
}
for (const model of chat) definition.finalize?.(model, ctx);
for (const model of chat) {
if (model.compat && Object.keys(model.compat).length === 0) delete model.compat;
applyImageResizeDefaults(model);
}
definition.finalizeAll?.(chat, ctx);
const image = definition.images?.(ctx) ?? [];
for (const model of image) applyImageResizeDefaults(model);
const classifier = definition.classifiers?.(ctx) ?? [];
catalogs[definition.id] = {
chat: chat.map((model) => ({ ...model, type: "chat" as const })),
image,
classifier,
};
}
return catalogs;
}

View file

@ -0,0 +1,142 @@
import type {
Api,
ClassifierApi,
ClassifierModel,
ImageApi,
ImageModel,
KnownApi,
Model,
ModelInputLimits,
ThinkingLevelMap,
} from "../src/types.ts";
import type { CatalogSources } from "./sources/index.ts";
import { type ModelsDevModel, modelsDevChatModels, modelsDevCost } from "./sources/models-dev.ts";
export type ChatModel = Model<Api>;
export type ModelCompat<TApi extends Api> = NonNullable<Model<TApi>["compat"]>;
/**
* Compat defaults per API. They sit under each model's own compat, so model
* values win. A function receives the model to vary defaults by model.
*/
export type ApiCompatDefaults = {
[TApi in KnownApi]?: ModelCompat<TApi> | ((model: Model<TApi>) => ModelCompat<TApi>);
};
/** Passed to every provider hook. */
export interface ProviderContext {
/** The provider being built. */
readonly providerId: string;
readonly sources: CatalogSources;
/** Fail instead of degrading when upstream data is unexpected. */
readonly strict: boolean;
/** Copies of another provider's chat models before shared rules run. */
providerModels(providerId: string): ChatModel[];
/** The models.dev entry a model was built from, if it was recorded. */
modelsDevSource(model: ChatModel): ModelsDevModel | undefined;
/** Record the models.dev entry so shared rules can use its reasoning metadata. */
recordModelsDevSource(model: ChatModel, source: ModelsDevModel): void;
}
/**
* One built-in provider's catalog. The build runs, in order:
* 1. `models()`: upstream models plus authored ones, with data corrections.
* 2. `compat` defaults and the shared model-family rules in `families/`.
* 3. `finalize()`: provider-specific behavior that must win over shared rules.
* 4. Shared normalization (image input limits), then `finalizeAll()`.
*/
export interface ProviderDefinition {
id: string;
models?(ctx: ProviderContext): ChatModel[];
images?(ctx: ProviderContext): ImageModel<ImageApi>[];
classifiers?(ctx: ProviderContext): ClassifierModel<ClassifierApi>[];
compat?: ApiCompatDefaults;
finalize?(model: ChatModel, ctx: ProviderContext): void;
finalizeAll?(models: ChatModel[], ctx: ProviderContext): void;
}
export function defineProvider(definition: ProviderDefinition): ProviderDefinition {
return definition;
}
/** Shallow-merge compat flags into a model. */
export function mergeCompat<TApi extends Api>(model: Model<TApi>, compat: ModelCompat<TApi>): void {
model.compat = { ...model.compat, ...compat } as ModelCompat<TApi>;
}
/** Shallow-merge thinking level mappings into a model. Later values win. */
export function mergeThinking(model: ChatModel, map: ThinkingLevelMap): void {
model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map };
}
/** Provider input limits for image-capable models. Limits the model already carries take precedence. */
export function withInputLimits(model: ChatModel, limits: ModelInputLimits): void {
if (!model.input.includes("image")) return;
model.inputLimits = {
...limits,
...model.inputLimits,
images: { ...limits.images, ...model.inputLimits?.images },
};
}
/** Add models only when upstream has not listed them (yet). */
export function addMissing(models: ChatModel[], additions: readonly ChatModel[]): ChatModel[] {
for (const addition of additions) {
if (!models.some((model) => model.id === addition.id)) models.push(addition);
}
return models;
}
export interface ModelsDevOptions<TApi extends Api> {
api: TApi;
baseUrl: string;
compat?: ModelCompat<TApi>;
headers?: Record<string, string>;
/** Only models models.dev lists on this API protocol (e.g. "responses"). */
protocol?: string;
/** Skip models models.dev marks deprecated. */
skipDeprecated?: boolean;
/** Expose models.dev effort levels through the shared rules. Defaults to true. */
recordReasoning?: boolean;
/** Fallback limits when models.dev has none. Defaults to 4096 each. */
defaultContextWindow?: number;
defaultMaxTokens?: number;
/** Extra filter. Models without tool support are always skipped. */
include?(source: ModelsDevModel): boolean;
/** Adjust or replace the mapped model. Return undefined to skip it. */
map?(model: Model<TApi>, source: ModelsDevModel): ChatModel | undefined;
}
/** Map one models.dev provider's tool-capable chat models onto pi models. */
export function modelsDevModels<TApi extends Api>(
ctx: ProviderContext,
sourceProvider: string,
options: ModelsDevOptions<TApi>,
): ChatModel[] {
const models: ChatModel[] = [];
for (const source of modelsDevChatModels(ctx.sources.modelsDev, sourceProvider)) {
if (!source.toolCall) continue;
if (options.protocol && !source.apis.some((api) => api.protocol === options.protocol)) continue;
if (options.skipDeprecated && source.status === "deprecated") continue;
if (options.include && !options.include(source)) continue;
const base: Model<TApi> = {
id: source.id,
name: source.name || source.id,
api: options.api,
provider: ctx.providerId,
baseUrl: options.baseUrl,
reasoning: source.reasoning,
input: source.imageInput ? ["text", "image"] : ["text"],
cost: modelsDevCost(source.cost),
contextWindow: source.contextWindow || options.defaultContextWindow || 4096,
maxTokens: source.maxTokens || options.defaultMaxTokens || 4096,
...(options.headers ? { headers: { ...options.headers } } : {}),
...(options.compat ? { compat: { ...options.compat } } : {}),
};
const model = options.map ? options.map(base, source) : base;
if (!model) continue;
if (options.recordReasoning !== false) ctx.recordModelsDevSource(model, source);
models.push(model);
}
return models;
}

View file

@ -0,0 +1,134 @@
// Anthropic Claude models: facts that hold on every host that serves them.
// Ids appear dashed (claude-opus-4-8), dotted (claude-opus-4.8), prefixed
// (anthropic/claude-opus-4.8), and dated (claude-opus-5-20260723).
import type { Model, ThinkingLevelMap } from "../../src/types.ts";
import { type ChatModel, mergeCompat, mergeThinking } from "../define.ts";
function stripPrefix(modelId: string): string {
return modelId.toLowerCase().replace(/^~?anthropic\//, "");
}
/** Models that accept per-message `output_config` effort changes. */
export function supportsClaudeMidConvoEffort(modelId: string): boolean {
const id = stripPrefix(modelId);
return (
/^claude-opus-(?:5|5[.-]5)(?:-\d{8})?$/.test(id) ||
/^claude-(?:sonnet|haiku)-5[.-]5(?:-\d{8})?$/.test(id) ||
/^claude-(?:fable|mythos)-5(?:[.-]1)(?:-\d{8})?$/.test(id)
);
}
// Claude models that accept `thinking: {type: "disabled"}` alongside per-message effort.
// Opus 5.5, Sonnet 5.5, Fable 5.1, and Mythos 5.1 reject it.
// https://platform.claude.com/docs/en/build-with-claude/thinking#configuring-thinking
const DISABLED_THINKING_MODELS = new Set(["claude-opus-5", "claude-haiku-5-5", "claude-haiku-5.5"]);
/** Models that accept system messages and tool changes after the conversation starts. */
export function supportsClaudeMidConvoSystemMessages(modelId: string): boolean {
return (
/^claude-opus-(?:4[.-]8|5(?:[.-]5)?)(?:-\d{8})?$/.test(modelId) ||
/^claude-(?:sonnet|haiku)-5[.-]5(?:-\d{8})?$/.test(modelId) ||
/^claude-(?:fable|mythos)-5(?:[.-]1)?(?:-\d{8})?$/.test(modelId)
);
}
/** Enable per-message effort changes. Models that cannot disable thinking with it lose "off". */
export function enableClaudeMidConvoEffort(model: Model<"anthropic-messages">): void {
mergeCompat(model, { supportsMidConvoEffort: true });
if (!DISABLED_THINKING_MODELS.has(stripPrefix(model.id).replace(/-\d{8}$/, ""))) {
mergeThinking(model, { off: null });
}
}
function idIncludesAny(modelId: string, fragments: readonly string[]): boolean {
return fragments.some((fragment) => modelId.includes(fragment));
}
const ADAPTIVE_THINKING = [
"opus-4-6",
"opus-4.6",
"opus-4-7",
"opus-4.7",
"opus-4-8",
"opus-4.8",
"opus-5",
"opus.5",
"sonnet-4-6",
"sonnet-4.6",
"sonnet-5",
"sonnet.5",
"haiku-5",
"haiku.5",
"fable-5",
"mythos-5",
];
/**
* OpenRouter's `~anthropic/claude-*-latest` aliases follow the newest model of each family,
* all of which use adaptive thinking.
*/
function isClaudeLatestAlias(modelId: string): boolean {
return /^~anthropic\/claude-[a-z]+-latest$/.test(modelId);
}
const TEMPERATURE_UNSUPPORTED = [
"opus-4-7",
"opus-4.7",
"opus-4-8",
"opus-4.8",
"opus-5",
"opus.5",
"sonnet-5-5",
"sonnet-5.5",
"haiku-5-5",
"haiku-5.5",
];
const MAX_EFFORT_ONLY = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"];
const XHIGH_AND_MAX_EFFORT = [
"opus-4-7",
"opus-4.7",
"opus-4-8",
"opus-4.8",
"opus-5",
"opus.5",
"sonnet-5",
"sonnet.5",
"haiku-5",
"haiku.5",
];
/**
* Adaptive thinking effort levels (per Anthropic adaptive thinking docs):
* "max" is available on all adaptive-thinking Claude models; "xhigh" only on
* Opus 4.7/4.8/5, Sonnet 5, Haiku 5.5, and Fable 5.
*/
export function applyClaudeFamilyRules(model: ChatModel): void {
if (idIncludesAny(model.id, MAX_EFFORT_ONLY)) mergeThinking(model, { max: "max" });
if (idIncludesAny(model.id, XHIGH_AND_MAX_EFFORT)) mergeThinking(model, { xhigh: "xhigh", max: "max" });
if (model.id.includes("fable-5")) mergeThinking(model, { off: null, xhigh: "xhigh", max: "max" });
if (model.api !== "anthropic-messages") return;
const messagesModel = model as Model<"anthropic-messages">;
if (isClaudeLatestAlias(model.id) || idIncludesAny(model.id, ADAPTIVE_THINKING)) {
mergeCompat(messagesModel, { forceAdaptiveThinking: true });
}
if (idIncludesAny(model.id.toLowerCase(), TEMPERATURE_UNSUPPORTED)) {
mergeCompat(messagesModel, { supportsTemperature: false });
}
}
/**
* models.dev may list Opus 5.5, Sonnet 5.5, and Haiku 5.5 before their effort
* metadata is complete. Haiku 5.5 accepts disabled thinking; Opus 5.5 and
* Sonnet 5.5 reject it.
*/
export function claude55ThinkingLevels(modelId: string): ThinkingLevelMap {
return {
...(modelId.includes("haiku") ? {} : { off: null }),
minimal: null,
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
max: "max",
};
}

View file

@ -0,0 +1,67 @@
// DeepSeek V4 models on OpenAI-compatible hosts.
import type { Model, OpenAICompletionsCompat } from "../../src/types.ts";
import { type ChatModel, mergeCompat, mergeThinking } from "../define.ts";
export const DEEPSEEK_COMPAT: OpenAICompletionsCompat = {
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
};
export const DEEPSEEK_V4_THINKING_LEVEL_MAP = {
minimal: null,
low: null,
medium: null,
high: "high",
max: "max",
} as const;
export const DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP = {
...DEEPSEEK_V4_THINKING_LEVEL_MAP,
low: "low",
} as const;
// Azure Foundry rejects DeepSeek's own max effort.
const AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP = {
minimal: null,
low: "low",
medium: "medium",
high: "high",
xhigh: null,
max: null,
} as const;
// Qwen Token Plan serves DeepSeek V4 through Qwen's own thinking format.
const QWEN_FORMAT_PROVIDERS = new Set(["qwen-token-plan", "qwen-token-plan-cn", "qwen-token-plan-individual"]);
// These hosts accept native reasoning efforts instead of DeepSeek's thinking toggle.
const NATIVE_EFFORT_PROVIDERS = new Set(["openrouter", "opencode"]);
function isDeepSeekV4(model: ChatModel): model is Model<"openai-completions"> {
return model.api === "openai-completions" && model.id.includes("deepseek-v4");
}
/** DeepSeek V4 needs its reasoning content replayed on every host. */
export function applyDeepSeekCompatRules(model: ChatModel): void {
if (!isDeepSeekV4(model) || QWEN_FORMAT_PROVIDERS.has(model.provider)) return;
mergeCompat(
model,
NATIVE_EFFORT_PROVIDERS.has(model.provider)
? { requiresReasoningContentOnAssistantMessages: DEEPSEEK_COMPAT.requiresReasoningContentOnAssistantMessages }
: DEEPSEEK_COMPAT,
);
}
/** DeepSeek V4 effort levels when the host did not report its own. */
export function applyDeepSeekFamilyRules(model: ChatModel): void {
if (!isDeepSeekV4(model) || model.thinkingLevelMap !== undefined) return;
if (model.provider === "openrouter") {
mergeThinking(model, { ...DEEPSEEK_V4_THINKING_LEVEL_MAP, xhigh: "xhigh", max: null });
} else if (model.provider === "azure") {
mergeThinking(model, AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP);
} else if (
(model.provider === "deepseek" || model.provider === "opencode" || model.provider === "opencode-go") &&
model.id.includes("deepseek-v4-flash")
) {
mergeThinking(model, DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP);
} else {
mergeThinking(model, DEEPSEEK_V4_THINKING_LEVEL_MAP);
}
}

View file

@ -0,0 +1,22 @@
// Google Gemini and Gemma models.
import type { ThinkingLevelMap } from "../../src/types.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
import type { ModelsDevReasoningOption } from "../sources/models-dev.ts";
function isGemma4Model(modelId: string): boolean {
return /gemma-?4/.test(modelId.toLowerCase());
}
/** Gemini effort levels from models.dev, with a Gemma 4 fallback. */
export function googleThinkingLevels(
modelId: string,
reasoningOptions: readonly ModelsDevReasoningOption[] = [],
): ThinkingLevelMap | undefined {
const effortMap = getEffortThinkingLevelMap(reasoningOptions);
if (effortMap) return effortMap;
if (isGemma4Model(modelId)) {
return { off: null, minimal: "MINIMAL", low: null, medium: null, high: "HIGH" };
}
return undefined;
}

View file

@ -0,0 +1,38 @@
// Moonshot Kimi models, shared by Moonshot, Kimi Coding, and gateways.
import type { ModelCost, OpenAICompletionsCompat } from "../../src/types.ts";
/**
* Kimi K3 and its point releases (kimi-k3.1-pro, kimi-k3.1-pro-highspeed) share
* K3's request format. https://api.moonshot.ai/v1/models reports effort levels
* and dynamic tool loading for all of them.
*/
export function isKimiK3FamilyId(modelId: string): boolean {
return /^kimi-k3(?:\.\d+)?(?:-|$)/.test(modelId);
}
export const KIMI_K3_MAX_TOKENS = 131072;
export const KIMI_K3_COST: ModelCost = {
input: 3,
output: 15,
cacheRead: 0.3,
cacheWrite: 0,
};
// Kimi Chat Completions settings shared by Moonshot and Kimi Coding.
export const KIMI_OPENAI_COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsStrictMode: false,
thinkingFormat: "deepseek",
};
// Kimi K3-style models take reasoning_effort and need earlier reasoning sent back.
export const KIMI_K3_OPENAI_COMPAT: OpenAICompletionsCompat = {
...KIMI_OPENAI_COMPAT,
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "openai",
supportsReasoningEffort: true,
};

View file

@ -0,0 +1,141 @@
// OpenAI GPT models: facts that hold on every host that serves them.
import type { Api, Model, ModelCost } from "../../src/types.ts";
import { type ChatModel, mergeCompat, mergeThinking } from "../define.ts";
import { roundCost } from "../sources/models-dev.ts";
export const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272000;
/** OpenAI bills prompts above 272K input tokens at 2x input and 1.5x output rates. */
export function withOpenAiLongContextPricing(cost: ModelCost): ModelCost {
return {
...cost,
tiers: [
{
inputTokensAbove: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
input: roundCost(cost.input * 2),
output: roundCost(cost.output * 1.5),
cacheRead: roundCost(cost.cacheRead * 2),
cacheWrite: roundCost(cost.cacheWrite * 2),
},
],
};
}
// Keep current OpenAI prices authoritative until models.dev and passthrough
// catalogs catch up.
// https://developers.openai.com/api/docs/pricing
export const OPENAI_STANDARD_COSTS: Record<string, ModelCost> = {
"gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
"gpt-5.6-sol": { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 },
"gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
"gpt-6-astra": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
"gpt-6-luna": { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 },
"gpt-6-sol": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"gpt-6.1-sol": { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 },
"gpt-6.1-terra": { input: 1, output: 5, cacheRead: 0.05, cacheWrite: 1.25 },
};
/** The standard price of an OpenAI model with long-context pricing. */
export function openAiStandardCost(id: string): ModelCost {
return withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS[id]);
}
// GPT-5.5 and older put tools loaded through tool search or additional_tools in a namespace named
// after the tool and reject replayed calls without it, so history from other models fails there
// (#11415). Like Codex, dynamic tool loading therefore starts with GPT-5.6, which does not do this.
/** Models that accept tool search and `additional_tools`. */
export const OPENAI_TOOL_SEARCH_MODEL_IDS = new Set([
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
/** Models that accept mid-conversation developer messages. */
export const OPENAI_MID_CONVO_MODEL_IDS = new Set([
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-pro",
"gpt-5.5",
...OPENAI_TOOL_SEARCH_MODEL_IDS,
]);
const RESPONSES_APIS = new Set<Api>(["openai-responses", "azure-openai-responses", "openai-codex-responses"]);
function isResponsesApi(model: ChatModel): boolean {
return RESPONSES_APIS.has(model.api);
}
export function supportsOpenAiXhigh(modelId: string): boolean {
return (
modelId.includes("gpt-5.2") ||
modelId.includes("gpt-5.3") ||
modelId.includes("gpt-5.4") ||
modelId.includes("gpt-5.5") ||
modelId.includes("gpt-5.6") ||
modelId.includes("gpt-6")
);
}
const GPT_6_MODEL_IDS = new Set(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-6.1-terra"]);
/** Thinking levels of GPT models on any host. */
export function applyOpenAiFamilyRules(model: ChatModel): void {
if ((model.api === "openai-responses" || model.api === "azure-openai-responses") && model.id.startsWith("gpt-5")) {
mergeThinking(model, { off: null });
}
if (GPT_6_MODEL_IDS.has(model.id) && isResponsesApi(model)) {
mergeThinking(model, {
// GPT-6 Astra and GPT-6.1 Sol reject reasoning.effort "none"; GPT-6.1 Terra accepts it. All reject
// "minimal" (verified on Codex, October 2026).
off: model.id === "gpt-6-astra" || model.id === "gpt-6.1-sol" ? null : "none",
minimal: null,
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
max: "max",
});
}
if (supportsOpenAiXhigh(model.id)) {
mergeThinking(model, { xhigh: "xhigh" });
}
if (
(model.id.includes("gpt-5.6") || model.id.includes("gpt-6")) &&
(isResponsesApi(model) || model.api === "openai-completions")
) {
mergeThinking(model, { max: "max" });
}
if (model.id.endsWith("gpt-5.5-pro")) {
mergeThinking(model, { off: null, minimal: null, low: null });
}
}
/**
* OpenAI custom grammar tools on Responses endpoints. OpenAI rejects
* `type: "custom"` tools for pre-GPT-5 models (gpt-4.x, gpt-4o, o-series).
*/
export function enableOpenAiGrammarTools(model: ChatModel): void {
if (!isResponsesApi(model)) return;
const match = /^gpt-(\d+)/.exec(model.id);
if (!match || Number(match[1]) < 5) return;
mergeCompat(model as Model<"openai-responses">, { supportsOpenAIGrammarTools: true });
}
/**
* Newer OpenAI Responses models accept developer messages after the
* conversation has started. Proxies that pass them through also pass
* `additional_tools` items through unchanged, for the models that accept them.
*/
export function enableOpenAiMidConvoSystemMessages(model: ChatModel, options: { proxy: boolean }): void {
if (model.api !== "openai-responses" && model.api !== "openai-codex-responses") return;
if (!OPENAI_MID_CONVO_MODEL_IDS.has(model.id)) return;
mergeCompat(model as Model<"openai-responses">, {
supportsMidConvoSystemMessages: true,
...(options.proxy && OPENAI_TOOL_SEARCH_MODEL_IDS.has(model.id) ? { supportsAdditionalTools: true } : {}),
});
}

View file

@ -0,0 +1,308 @@
#!/usr/bin/env node
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
createModelDataManifest,
groupProviderModelData,
MODEL_DATA_MANIFEST_FILE,
type ModelDataStructure,
readModelDataProviderIds,
validateGeneratedModelData,
validateModelDataDirectory,
} from "../scripts/model-data.ts";
import type {
AnyModel,
Api,
ClassifierApi,
ClassifierModel,
ImageApi,
ImageModel,
Model,
ModelType,
} from "../src/types.ts";
import { buildCatalog, type ProviderCatalog } from "./build.ts";
import { PROVIDERS } from "./providers/index.ts";
import { loadCatalogSources } from "./sources/index.ts";
/** A catalog entry with its model type spelled out. */
type CatalogEntry = AnyModel & { type: ModelType };
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
interface GeneratorOptions {
strict: boolean;
dataOnly: boolean;
jsonOnly: boolean;
jsonOutputDir: string | undefined;
pretty: boolean;
}
function readGeneratorOptions(args: string[]): GeneratorOptions {
let strict = false;
let dataOnly = false;
let jsonOnly = false;
let jsonOutputDir: string | undefined;
let pretty = false;
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--strict") {
strict = true;
continue;
}
if (arg === "--data-only") {
dataOnly = true;
continue;
}
if (arg === "--json-only") {
jsonOnly = true;
continue;
}
if (arg === "--pretty") {
pretty = true;
continue;
}
if (arg === "--json-output") {
const value = args[++index];
if (!value) throw new Error("--json-output requires a directory");
jsonOutputDir = resolve(value);
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (jsonOnly && !jsonOutputDir) throw new Error("--json-only requires --json-output");
if (dataOnly && (jsonOnly || jsonOutputDir))
throw new Error("--data-only cannot be combined with JSON catalog output");
return { strict, dataOnly, jsonOnly, jsonOutputDir, pretty };
}
/** Key models by id, keeping the first entry per id, sorted by id. */
function sortedById<T extends { id: string }>(models: readonly T[]): [string, T][] {
const byId = new Map<string, T>();
for (const model of models) if (!byId.has(model.id)) byId.set(model.id, model);
return [...byId].sort(([left], [right]) => left.localeCompare(right));
}
async function generateModels(generatorOptions: GeneratorOptions): Promise<void> {
const sources = await loadCatalogSources(generatorOptions.strict);
const catalogs = buildCatalog(PROVIDERS, sources, generatorOptions.strict);
// Providers without any models are left out, as if they did not exist.
const providers: Record<string, ProviderCatalog> = Object.fromEntries(
Object.entries(catalogs).filter(
([, catalog]) => catalog.chat.length + catalog.image.length + catalog.classifier.length > 0,
),
);
const sortedProviderIds = Object.keys(providers).sort();
const jsonChatProviders: Record<string, Record<string, Model<Api> & { type: "chat" }>> = {};
const jsonImageProviders: Record<string, Record<string, ImageModel<ImageApi>>> = {};
const jsonClassifierProviders: Record<string, Record<string, ClassifierModel<ClassifierApi>>> = {};
const jsonAllProviders: Record<string, CatalogEntry[]> = {};
for (const providerId of sortedProviderIds) {
jsonChatProviders[providerId] = Object.fromEntries(sortedById(providers[providerId].chat));
jsonImageProviders[providerId] = Object.fromEntries(sortedById(providers[providerId].image));
jsonClassifierProviders[providerId] = Object.fromEntries(sortedById(providers[providerId].classifier));
jsonAllProviders[providerId] = [
...Object.values(jsonChatProviders[providerId]),
...Object.values(jsonImageProviders[providerId]),
...Object.values(jsonClassifierProviders[providerId]),
];
}
const serializeJson = (value: unknown) =>
`${JSON.stringify(value, null, generatorOptions.pretty ? 2 : undefined)}\n`;
const writeJson = (path: string, value: unknown) => writeFileSync(path, serializeJson(value));
const generatedDataProviderIds = generatorOptions.dataOnly
? readModelDataProviderIds(packageRoot)
: sortedProviderIds;
const missingProviderIds = generatedDataProviderIds.filter((providerId) => !jsonAllProviders[providerId]);
if (missingProviderIds.length > 0) {
throw new Error(`Cannot hydrate missing providers: ${missingProviderIds.join(", ")}`);
}
// Only the ignored internal data is grouped by API for type derivation.
const generatedDataProviders: Record<string, Record<string, Record<string, CatalogEntry>>> = {};
const modelDataStructure: ModelDataStructure = {};
for (const providerId of generatedDataProviderIds) {
const { groups, structure } = groupProviderModelData(providerId, jsonAllProviders[providerId]);
generatedDataProviders[providerId] = groups;
modelDataStructure[providerId] = structure;
}
const generatedAt = new Date().toISOString();
if (!generatorOptions.jsonOnly) {
// Stage and validate all provider values before replacing the current generated data.
const providersDir = join(packageRoot, "src/providers");
const dataDir = join(providersDir, "data");
const stagingRoot = mkdtempSync(join(providersDir, ".model-generation-"));
const stagedDataDir = join(stagingRoot, "data");
const previousDataDir = join(stagingRoot, "previous-data");
let restoreGeneratedCatalog: (() => void) | undefined;
try {
mkdirSync(stagedDataDir, { recursive: true });
const fileContents: Record<string, string> = {};
for (const providerId of generatedDataProviderIds) {
const filename = `${providerId}.json`;
const content = serializeJson(generatedDataProviders[providerId]);
fileContents[filename] = content;
writeFileSync(join(stagedDataDir, filename), content);
}
writeJson(
join(stagedDataDir, MODEL_DATA_MANIFEST_FILE),
createModelDataManifest(modelDataStructure, fileContents, generatedAt),
);
validateModelDataDirectory(modelDataStructure, stagedDataDir);
if (!generatorOptions.dataOnly) {
const previousShardContents = new Map(
readdirSync(providersDir)
.filter((entry) => entry.endsWith(".models.ts"))
.map((entry) => [entry, readFileSync(join(providersDir, entry), "utf8")] as const),
);
const aggregatorPath = join(packageRoot, "src/models.generated.ts");
const previousAggregator = readFileSync(aggregatorPath, "utf8");
restoreGeneratedCatalog = () => {
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts")) rmSync(join(providersDir, entry));
}
for (const [entry, content] of previousShardContents) {
writeFileSync(join(providersDir, entry), content);
}
writeFileSync(aggregatorPath, previousAggregator);
};
const generatedHeader = `// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
`;
const catalogConstName = (providerId: string) =>
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
const imageCatalogConstName = (providerId: string) =>
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_IMAGE_MODELS`;
const classifierCatalogConstName = (providerId: string) =>
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_CLASSIFIER_MODELS`;
const generatedShardFiles = new Set<string>();
for (const providerId of sortedProviderIds) {
let output = generatedHeader;
output += `import values from "./data/${providerId}.json" with { type: "json" };\n`;
output += `import { flattenChatModelCatalog, flattenClassifierModelCatalog, flattenImageModelCatalog, type ChatModelCatalog, type ClassifierModelCatalog, type ImageModelCatalog } from "../model-catalog.ts";\n\n`;
output += `export const ${catalogConstName(providerId)}: ChatModelCatalog<typeof values, ${JSON.stringify(providerId)}> =\n`;
output += `\tflattenChatModelCatalog(${JSON.stringify(providerId)}, values);\n\n`;
output += `export const ${imageCatalogConstName(providerId)}: ImageModelCatalog<typeof values, ${JSON.stringify(providerId)}> =\n`;
output += `\tflattenImageModelCatalog(${JSON.stringify(providerId)}, values);\n\n`;
output += `export const ${classifierCatalogConstName(providerId)}: ClassifierModelCatalog<typeof values, ${JSON.stringify(providerId)}> =\n`;
output += `\tflattenClassifierModelCatalog(${JSON.stringify(providerId)}, values);\n`;
const filename = `${providerId}.models.ts`;
generatedShardFiles.add(filename);
writeFileSync(join(providersDir, filename), output);
}
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts") && !generatedShardFiles.has(entry)) rmSync(join(providersDir, entry));
}
let output = generatedHeader;
for (const providerId of sortedProviderIds) {
output += `import { ${classifierCatalogConstName(providerId)}, ${imageCatalogConstName(providerId)}, ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
}
output += `\nexport const MODELS: {\n`;
for (const providerId of sortedProviderIds) {
output += `\treadonly ${JSON.stringify(providerId)}: typeof ${catalogConstName(providerId)};\n`;
}
output += `} = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
}
output += `};\n\nexport const IMAGE_MODELS: {\n`;
for (const providerId of sortedProviderIds) {
output += `\treadonly ${JSON.stringify(providerId)}: typeof ${imageCatalogConstName(providerId)};\n`;
}
output += `} = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${imageCatalogConstName(providerId)},\n`;
}
output += `};\n\nexport const CLASSIFIER_MODELS: {\n`;
for (const providerId of sortedProviderIds) {
output += `\treadonly ${JSON.stringify(providerId)}: typeof ${classifierCatalogConstName(providerId)};\n`;
}
output += `} = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${classifierCatalogConstName(providerId)},\n`;
}
output += `};\n`;
writeFileSync(aggregatorPath, output);
console.log("Generated provider catalogs and src/models.generated.ts");
}
const hadPreviousData = existsSync(dataDir);
if (hadPreviousData) renameSync(dataDir, previousDataDir);
try {
renameSync(stagedDataDir, dataDir);
validateGeneratedModelData(packageRoot);
} catch (error) {
rmSync(dataDir, { recursive: true, force: true });
if (hadPreviousData && existsSync(previousDataDir)) renameSync(previousDataDir, dataDir);
throw error;
}
restoreGeneratedCatalog = undefined;
console.log(
generatorOptions.dataOnly
? "Hydrated JSON model values under src/providers/data/"
: "Generated JSON model values under src/providers/data/",
);
} catch (error) {
restoreGeneratedCatalog?.();
throw error;
} finally {
rmSync(stagingRoot, { recursive: true, force: true });
}
}
if (generatorOptions.jsonOutputDir) {
// `models.json` and `providers/{id}.json` retain the legacy keyed chat catalog.
// The `.all` variants are arrays so the same upstream id can appear once per type.
const providerOutputDir = join(generatorOptions.jsonOutputDir, "providers");
rmSync(generatorOptions.jsonOutputDir, { recursive: true, force: true });
mkdirSync(providerOutputDir, { recursive: true });
writeJson(join(generatorOptions.jsonOutputDir, "models.json"), jsonChatProviders);
writeJson(join(generatorOptions.jsonOutputDir, "models.all.json"), jsonAllProviders);
writeJson(join(generatorOptions.jsonOutputDir, "providers.json"), sortedProviderIds);
for (const providerId of sortedProviderIds) {
writeJson(join(providerOutputDir, `${providerId}.json`), jsonChatProviders[providerId]);
writeJson(join(providerOutputDir, `${providerId}.all.json`), jsonAllProviders[providerId]);
}
console.log(`Generated JSON model catalog under ${generatorOptions.jsonOutputDir}`);
}
// Print statistics
const chatModels = Object.values(providers).flatMap((catalog) => catalog.chat);
const totalModels = chatModels.length;
const reasoningModels = chatModels.filter((model) => model.reasoning).length;
console.log(`\nModel Statistics:`);
console.log(` Total tool-capable models: ${totalModels}`);
console.log(` Reasoning-capable models: ${reasoningModels}`);
for (const [provider, models] of Object.entries(providers)) {
console.log(
` ${provider}: ${models.chat.length} chat models, ${models.image.length} image models, ${models.classifier.length} classifier models`,
);
}
}
generateModels(readGeneratorOptions(process.argv.slice(2))).catch((error) => {
console.error(error);
process.exitCode = 1;
});

View file

@ -0,0 +1,32 @@
import { defineProvider, modelsDevModels, withInputLimits } from "../define.ts";
// Opus 5 is only callable through cross-region inference profiles.
const INFERENCE_PROFILE_ONLY_MODEL_IDS = new Set(["anthropic.claude-opus-5"]);
function bedrockBaseUrl(modelId: string): string {
return modelId.startsWith("eu.")
? "https://bedrock-runtime.eu-central-1.amazonaws.com"
: "https://bedrock-runtime.us-east-1.amazonaws.com";
}
export default defineProvider({
id: "amazon-bedrock",
models: (ctx) =>
modelsDevModels(ctx, "amazon-bedrock", {
api: "bedrock-converse-stream",
baseUrl: "",
include: (source) =>
!INFERENCE_PROFILE_ONLY_MODEL_IDS.has(source.id) &&
// Jamba does not support tool use in streaming mode.
!source.id.startsWith("ai21.jamba") &&
// Mistral 7B Instruct does not support system messages.
!source.id.startsWith("mistral.mistral-7b-instruct-v0"),
// Includes models.dev pricing tiers, e.g. the long-context tier for OpenAI models (#10326).
map: (model, source) => ({
...model,
baseUrl: bedrockBaseUrl(model.id),
...(source.structuredOutput ? { compat: { supportsStrictMode: true } } : {}),
}),
}),
finalize: (model) => withInputLimits(model, { images: { maxPerMessage: 20 } }),
});

View file

@ -0,0 +1,52 @@
import type { Model, OpenAICompletionsCompat } from "../../src/types.ts";
import { defineProvider, mergeThinking } from "../define.ts";
const COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsLongCacheRetention: false,
};
function model(id: string, name: string, reasoning: boolean, cost: Model<"openai-completions">["cost"]) {
return {
id,
name,
api: "openai-completions",
baseUrl: "https://api.ant-ling.com/v1",
provider: "ant-ling",
reasoning,
input: ["text"],
cost,
contextWindow: 262144,
maxTokens: 65536,
compat: reasoning ? { ...COMPAT, thinkingFormat: "ant-ling" } : COMPAT,
} satisfies Model<"openai-completions">;
}
export default defineProvider({
id: "ant-ling",
models: () => [
model("Ling-2.6-flash", "Ling 2.6 Flash", false, { input: 0.01, output: 0.02, cacheRead: 0, cacheWrite: 0 }),
model("Ling-2.6-1T", "Ling 2.6 1T", false, { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 }),
model("Ring-2.6-1T", "Ring 2.6 1T", true, { input: 0.06, output: 0.25, cacheRead: 0, cacheWrite: 0 }),
],
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
thinkingFormat: "ant-ling",
supportsStrictMode: true,
supportsLongCacheRetention: false,
},
},
finalize: (model) => {
// Ring reasons by default. Only high/xhigh have documented explicit effort controls.
if (model.reasoning) {
mergeThinking(model, { off: null, minimal: null, low: null, medium: null, high: "high", xhigh: "xhigh" });
}
},
});

View file

@ -0,0 +1,66 @@
import type { Model } from "../../src/types.ts";
import { defineProvider, mergeCompat, mergeThinking, modelsDevModels, withInputLimits } from "../define.ts";
import {
claude55ThinkingLevels,
enableClaudeMidConvoEffort,
supportsClaudeMidConvoEffort,
supportsClaudeMidConvoSystemMessages,
} from "../families/anthropic.ts";
// Models the API may fall back to, with the price the caller is billed.
const ALLOWED_FALLBACK_MODELS: Record<string, string[]> = {
"claude-fable-5": ["claude-opus-4-8", "claude-opus-5"],
"claude-opus-5": ["claude-opus-4-8"],
};
const MODELS_WITH_1M_CONTEXT = new Set(["claude-opus-4-6", "claude-sonnet-4-6"]);
const MODELS_55 = new Set(["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5"]);
export default defineProvider({
id: "anthropic",
models: (ctx) =>
modelsDevModels(ctx, "anthropic", {
api: "anthropic-messages",
baseUrl: "https://api.anthropic.com",
map: (model) => {
if (MODELS_55.has(model.id)) mergeThinking(model, claude55ThinkingLevels(model.id));
if (MODELS_WITH_1M_CONTEXT.has(model.id)) model.contextWindow = 1000000;
return model;
},
}),
finalize: (model) => {
if (model.api !== "anthropic-messages") return;
const messagesModel = model as Model<"anthropic-messages">;
if (supportsClaudeMidConvoEffort(model.id)) enableClaudeMidConvoEffort(messagesModel);
if (supportsClaudeMidConvoSystemMessages(model.id)) {
mergeCompat(messagesModel, { supportsMidConvoSystemMessages: true, supportsMidConvoToolChanges: true });
}
mergeCompat(messagesModel, { supportsStrictTools: true });
// Ephemeral entries have a hard five-minute lifetime; `ttl: "1h"` extends
// it to one hour. Only direct Anthropic is annotated so cache warming does
// not assume equivalent behavior through proxies.
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
model.promptCache = { short: 300, long: 3600 };
withInputLimits(model, {
maxRequestBytes: 32 * 1024 * 1024,
images: { maxPerRequest: model.contextWindow === 200000 ? 100 : 600 },
});
},
finalizeAll: (models) => {
const modelsById = new Map(models.map((model) => [model.id, model as Model<"anthropic-messages">]));
for (const [modelId, fallbackModelIds] of Object.entries(ALLOWED_FALLBACK_MODELS)) {
const model = modelsById.get(modelId);
if (!model) continue;
const compatibleFallbackModelIds = model.compat?.supportsMidConvoEffort
? fallbackModelIds.filter(supportsClaudeMidConvoEffort)
: fallbackModelIds;
const allowedFallbackModels = compatibleFallbackModelIds.flatMap((fallbackModelId) => {
const fallbackModel = modelsById.get(fallbackModelId);
return fallbackModel
? [{ provider: fallbackModel.provider, model: fallbackModel.id, cost: fallbackModel.cost }]
: [];
});
if (allowedFallbackModels.length > 0) mergeCompat(model, { allowedFallbackModels });
}
},
});

View file

@ -0,0 +1,61 @@
import type { Model, ModelCost } from "../../src/types.ts";
import { type ChatModel, defineProvider, mergeCompat } from "../define.ts";
import { enableOpenAiGrammarTools } from "../families/openai.ts";
// Azure Foundry deploys these with larger context windows than OpenAI's own short-tier defaults.
// See models-sold-directly-by-azure docs.
const CONTEXT_WINDOW_OVERRIDES: Record<string, number> = {
"gpt-5.4": 1050000,
"gpt-5.5": 1050000,
"gpt-5.6-luna": 1050000,
"gpt-5.6-sol": 1050000,
"gpt-5.6-terra": 1050000,
};
// Azure resells DeepSeek at its own rates. US data zone, checked 2026-09-16.
// https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/deepseek/
const DEEPSEEK_V4_PRO_COST: ModelCost = { input: 1.925, output: 3.828, cacheRead: 0.165, cacheWrite: 0 };
/** OpenAI models as Azure deploys them. Azure bills without OpenAI's long-context tier. */
function openAiModels(models: ChatModel[]): ChatModel[] {
return models
.filter((model) => model.api === "openai-responses")
.map((model) => ({
...model,
api: "azure-openai-responses",
provider: "azure",
baseUrl: "",
cost: {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite,
},
contextWindow: CONTEXT_WINDOW_OVERRIDES[model.id] ?? model.contextWindow,
}));
}
export default defineProvider({
id: "azure",
models: (ctx) => [
...openAiModels(ctx.providerModels("openai")),
...ctx
.providerModels("deepseek")
.filter((model) => model.id === "deepseek-v4-pro")
.map((model) => ({ ...model, provider: "azure", baseUrl: "", cost: DEEPSEEK_V4_PRO_COST })),
],
compat: { "openai-completions": { supportsStrictMode: true } },
finalize: (model) => {
enableOpenAiGrammarTools(model);
if (model.api === "openai-completions") {
// Azure 400s on DeepSeek's `thinking` field and on every prompt cache parameter, discards a
// `developer` system message unbilled once reasoning_effort is set, and honours mid-convo ones (#9645).
mergeCompat(model as Model<"openai-completions">, {
supportsDeveloperRole: false,
supportsMidConvoSystemMessages: true,
thinkingFormat: "openai",
supportsLongCacheRetention: false,
});
}
},
});

View file

@ -0,0 +1,89 @@
import type { OpenAICompletionsCompat, ThinkingLevelMap } from "../../src/types.ts";
import { defineProvider, modelsDevModels } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
const BASE_COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
supportsUsageInStreaming: true,
maxTokensField: "max_tokens",
supportsStrictMode: true,
// Baseten automatic prompt caching needs session affinity so related
// requests land on the same replica. See:
// https://docs.baseten.co/inference/model-apis/pricing-and-limits
sendSessionAffinityHeaders: true,
supportsLongCacheRetention: false,
};
const REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
...BASE_COMPAT,
supportsReasoningEffort: true,
thinkingFormat: "openai",
};
const TOGGLE_REASONING_COMPAT: OpenAICompletionsCompat = {
...BASE_COMPAT,
thinkingFormat: "baseten",
chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } },
};
const TOGGLE_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
...REASONING_EFFORT_COMPAT,
thinkingFormat: "baseten",
chatTemplateArgs: { enable_thinking: { $var: "thinking.enabled" } },
};
const TOGGLE_LEVELS: ThinkingLevelMap = {
off: "off",
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: null,
};
const GLM_52_LEVELS: ThinkingLevelMap = {
off: "none",
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: "max",
};
const GLM_52_MODEL_IDS = new Set(["zai-org/GLM-5.2", "zai-org/GLM-5.2-Fast"]);
export default defineProvider({
id: "baseten",
models: (ctx) =>
modelsDevModels(ctx, "baseten", {
api: "openai-completions",
baseUrl: "https://inference.baseten.co/v1",
skipDeprecated: true,
recordReasoning: false,
map: (model, source) => {
const options = source.reasoningOptions ?? [];
const isGlm52 = GLM_52_MODEL_IDS.has(model.id);
const supportsToggle = isGlm52 || options.some((option) => option.type === "toggle");
const supportsEffort = isGlm52 || options.some((option) => option.type === "effort");
const compat =
supportsToggle && supportsEffort
? TOGGLE_REASONING_EFFORT_COMPAT
: supportsToggle
? TOGGLE_REASONING_COMPAT
: supportsEffort
? REASONING_EFFORT_COMPAT
: BASE_COMPAT;
const thinkingLevelMap = isGlm52
? GLM_52_LEVELS
: supportsToggle
? TOGGLE_LEVELS
: getEffortThinkingLevelMap(options);
return {
...model,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
// Baseten's GLM-5.2 endpoints are text-only despite models.dev reporting image input.
input: !isGlm52 && source.imageInput ? ["text", "image"] : ["text"],
compat,
};
},
}),
compat: { "openai-completions": { supportsStrictMode: true } },
});

View file

@ -0,0 +1,8 @@
import { defineProvider, modelsDevModels } from "../define.ts";
export default defineProvider({
id: "cerebras",
models: (ctx) =>
modelsDevModels(ctx, "cerebras", { api: "openai-completions", baseUrl: "https://api.cerebras.ai/v1" }),
compat: { "openai-completions": { supportsStore: false, supportsDeveloperRole: false } },
});

View file

@ -0,0 +1,84 @@
import {
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL,
} from "../../src/api/cloudflare.ts";
import type { Model } from "../../src/types.ts";
import { type ChatModel, defineProvider, mergeCompat, modelsDevModels } from "../define.ts";
import { enableOpenAiGrammarTools, OPENAI_STANDARD_COSTS, withOpenAiLongContextPricing } from "../families/openai.ts";
/** models.dev lists gateway models as `<upstream>/<id>`; route each upstream to its passthrough. */
function routeGatewayModel(model: ChatModel): ChatModel | undefined {
const slash = model.id.indexOf("/");
if (slash === -1) return undefined;
const upstream = model.id.slice(0, slash);
const nativeId = model.id.slice(slash + 1);
if (upstream === "openai") {
// Cloudflare AI Gateway passes OpenAI usage through at OpenAI list prices.
const standardCost = OPENAI_STANDARD_COSTS[nativeId];
return {
...model,
id: nativeId,
api: "openai-responses",
baseUrl: CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL,
...(standardCost ? { cost: withOpenAiLongContextPricing(standardCost) } : {}),
};
}
if (upstream === "anthropic") {
// The /anthropic passthrough forwards the model ID to Anthropic unchanged.
// models.dev lists dotted versions (claude-opus-5.5), but Anthropic only
// accepts dashed IDs (claude-opus-5-5).
return {
...model,
id: nativeId.replaceAll(".", "-"),
api: "anthropic-messages",
baseUrl: CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
// Gateway passthroughs forward session affinity headers to upstreams that
// use them for cache/routing affinity.
compat: { sendSessionAffinityHeaders: true },
};
}
if (upstream === "workers-ai") {
return { ...model, compat: { sendSessionAffinityHeaders: true } };
}
return undefined;
}
export default defineProvider({
id: "cloudflare-ai-gateway",
models: (ctx) => {
const models = modelsDevModels(ctx, "cloudflare-ai-gateway", {
api: "openai-completions",
baseUrl: CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
map: routeGatewayModel,
});
// The gateway proxies Workers AI through its OpenAI-compatible /compat endpoint,
// but models.dev may omit or intermittently drop those `workers-ai/*` entries
// from the AI Gateway catalog. Mirror the Workers AI catalog under the documented
// prefix so the gateway keeps its OpenAI-compatible models stable.
const ids = new Set(models.map((model) => model.id));
const mirrored = modelsDevModels(ctx, "cloudflare-workers-ai", {
api: "openai-completions",
baseUrl: CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
compat: { sendSessionAffinityHeaders: true },
include: (source) => !ids.has(`workers-ai/${source.id}`),
map: (model) => ({ ...model, id: `workers-ai/${model.id}` }),
});
return [...models, ...mirrored];
},
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsLongCacheRetention: false,
},
},
finalize: (model) => {
if (model.api === "openai-responses") {
mergeCompat(model as Model<"openai-responses">, { supportsStrictMode: true });
}
enableOpenAiGrammarTools(model);
},
});

View file

@ -0,0 +1,61 @@
import { CLOUDFLARE_WORKERS_AI_BASE_URL, CLOUDFLARE_WORKERS_AI_REST_BASE_URL } from "../../src/api/cloudflare.ts";
import type { ClassifierModel } from "../../src/types.ts";
import { defineProvider, modelsDevModels } from "../define.ts";
function classifier(
id: string,
name: string,
input: number,
contextWindow: number,
imageInput: "images" | "assets",
): ClassifierModel<"cloudflare-workers-ai-system-one"> {
return {
type: "classifier",
id,
name,
api: "cloudflare-workers-ai-system-one",
provider: "cloudflare-workers-ai",
baseUrl: CLOUDFLARE_WORKERS_AI_REST_BASE_URL,
input: ["text", "image"],
cost: { input, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow,
compat: { imageInput },
};
}
// Workers AI has no unauthenticated catalog, so its System One models are listed here from
// Cloudflare's model pages.
const CLASSIFIER_MODELS = [
// Cloudflare-hosted Clef decision models. They take up to four PNG, JPEG, or WebP images as data URLs.
// Clef Omni also takes audio and video, which classifier contexts do not carry.
// Pricing and context: https://developers.cloudflare.com/workers-ai/models/clef/,
// https://developers.cloudflare.com/workers-ai/models/clef-flash/,
// and https://developers.cloudflare.com/workers-ai/models/clef-omni/
classifier("@cf/cloudflare/clef", "Clef", 0.24, 65536, "images"),
classifier("@cf/cloudflare/clef-flash", "Clef Flash", 0.038, 24576, "images"),
classifier("@cf/cloudflare/clef-omni", "Clef Omni", 0.15, 64000, "images"),
// TypeSafe's Jev models take PNG and JPEG images as System One assets.
// https://developers.cloudflare.com/ai/models/typesafe/jev/ (jev-latest)
// and https://developers.cloudflare.com/ai/models/typesafe/jev-1.14.0/
classifier("typesafe/jev", "Jev", 0.042, 32000, "assets"),
classifier("typesafe/jev-1.14.0", "Jev 1.14", 0.042, 32000, "assets"),
];
export default defineProvider({
id: "cloudflare-workers-ai",
models: (ctx) =>
modelsDevModels(ctx, "cloudflare-workers-ai", {
api: "openai-completions",
baseUrl: CLOUDFLARE_WORKERS_AI_BASE_URL,
compat: { sendSessionAffinityHeaders: true },
}),
classifiers: () => CLASSIFIER_MODELS,
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
supportsStrictMode: true,
supportsLongCacheRetention: false,
},
},
});

View file

@ -0,0 +1,57 @@
import type { Model } from "../../src/types.ts";
import { defineProvider, mergeCompat } from "../define.ts";
import { DEEPSEEK_COMPAT, DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP } from "../families/deepseek.ts";
// models.dev's DeepSeek entries lag the API; pricing from
// https://api-docs.deepseek.com/quick_start/pricing. DeepSeek also offers
// time-based off-peak rates, which the cost schema cannot represent yet.
const MODELS: Model<"openai-completions">[] = [
{
id: "deepseek-flash",
name: "DeepSeek V4.1 Flash",
api: "openai-completions",
baseUrl: "https://api.deepseek.com",
provider: "deepseek",
reasoning: true,
thinkingLevelMap: DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP,
input: ["text", "image"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 384000,
compat: DEEPSEEK_COMPAT,
},
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
api: "openai-completions",
baseUrl: "https://api.deepseek.com",
provider: "deepseek",
reasoning: true,
input: ["text"],
cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 384000,
compat: DEEPSEEK_COMPAT,
},
];
export default defineProvider({
id: "deepseek",
models: () => structuredClone(MODELS),
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
maxTokensField: "max_tokens",
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
supportsStrictMode: true,
},
},
finalize: (model) => {
// DeepSeek V4 Pro accepts plain system text in place mid-conversation.
if (model.id === "deepseek-v4-pro") {
mergeCompat(model as Model<"openai-completions">, { supportsMidConvoSystemMessages: true });
}
},
});

View file

@ -0,0 +1,100 @@
import type { AnthropicMessagesCompat, Model, OpenAICompletionsCompat } from "../../src/types.ts";
import { defineProvider, mergeCompat, mergeThinking, modelsDevModels } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
// Fireworks Anthropic-compatible API. Prompt caching uses automatic prefix
// matching and session affinity: x-session-affinity routes requests to the same
// replica for cache hits. cache_control on tools and eager_input_streaming are
// not supported.
// https://docs.fireworks.ai/tools-sdks/anthropic-compatibility
const ANTHROPIC_COMPAT: AnthropicMessagesCompat = {
allowEmptySignature: true,
sendSessionAffinityHeaders: true,
supportsEagerToolInputStreaming: false,
supportsCacheControlOnTools: false,
supportsLongCacheRetention: false,
};
const OPENAI_COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
supportsDeveloperRole: false,
sendSessionAffinityHeaders: true,
supportsLongCacheRetention: false,
};
const KIMI_K3_COMPAT: OpenAICompletionsCompat = {
...OPENAI_COMPAT,
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "openai",
};
// Verified against Fireworks Messages raw_output on 2026-09-10 (#9323).
// Fall back to verified support when models.dev omits effort metadata; this is
// not an allowlist. Any Fireworks Messages model advertising effort uses adaptive thinking.
const ADAPTIVE_THINKING_FALLBACK_MODELS = new Set([
"accounts/fireworks/models/deepseek-v4-flash-0731",
"accounts/fireworks/models/deepseek-v4-flash-vision-exp",
"accounts/fireworks/models/deepseek-v4-pro-0813",
"accounts/fireworks/models/qwen3p8-max",
"accounts/fireworks/models/qwen3p8-2p4t-a95b",
]);
export default defineProvider({
id: "fireworks",
models: (ctx) =>
modelsDevModels(ctx, "fireworks-ai", {
api: "anthropic-messages",
// SDK appends /v1/messages.
baseUrl: "https://api.fireworks.ai/inference",
map: (model, source) => {
if (model.id.includes("glm-") || model.id.includes("kimi-k3")) {
return {
...model,
api: "openai-completions",
baseUrl: "https://api.fireworks.ai/inference/v1",
compat: model.id.includes("kimi-k3") ? KIMI_K3_COMPAT : OPENAI_COMPAT,
};
}
// Use adaptive thinking for cataloged effort controls, with verified
// fallbacks where models.dev is incomplete. New models need no allowlist entry.
const adaptive =
source.reasoningOptions?.some((option) => option.type === "effort") ||
ADAPTIVE_THINKING_FALLBACK_MODELS.has(model.id);
return { ...model, compat: { ...ANTHROPIC_COMPAT, ...(adaptive ? { forceAdaptiveThinking: true } : {}) } };
},
}),
compat: { "openai-completions": { supportsStrictMode: true } },
finalize: (model, ctx) => {
if (model.api === "anthropic-messages" && (model as Model<"anthropic-messages">).compat?.forceAdaptiveThinking) {
// Qwen Max currently advertises only a toggle. Prefer upstream effort
// metadata once available instead of replacing it with this fallback.
if (model.id === "accounts/fireworks/models/qwen3p8-max" && !model.thinkingLevelMap) {
model.thinkingLevelMap = getEffortThinkingLevelMap([
{ type: "effort", values: ["low", "medium", "xhigh"] },
]);
}
if (
ctx.modelsDevSource(model)?.reasoningOptions?.some((option) => option.type === "toggle") ||
// The 2.4T alias omits the verified toggle in models.dev.
model.id === "accounts/fireworks/models/qwen3p8-2p4t-a95b"
) {
mergeThinking(model, { off: "none" });
}
if (model.id === "accounts/fireworks/models/deepseek-v4-pro-0813") mergeThinking(model, { low: "low" });
}
if (model.id.includes("glm-5p2")) {
// GLM 5.2 and its fast router support off/high/max. Fireworks maps low
// and medium to high, so do not expose those aliases as distinct levels.
mergeThinking(model, { off: "none", minimal: null, low: null, medium: null, max: "max" });
}
if (model.id.includes("kimi-k3")) {
// Fireworks maps medium to high on both APIs; do not expose it as a distinct level.
mergeThinking(model, { medium: null });
// Kimi K3 accepts tool-bearing system messages; Fireworks passes them through.
if (model.api === "openai-completions") {
mergeCompat(model as Model<"openai-completions">, {
supportsMidConvoSystemMessages: true,
supportsMidConvoToolAdditions: true,
});
}
}
},
});

View file

@ -0,0 +1,122 @@
import type { Api, Model, ThinkingLevelMap } from "../../src/types.ts";
import { addMissing, type ChatModel, defineProvider, mergeCompat, mergeThinking, modelsDevModels } from "../define.ts";
import { claude55ThinkingLevels, supportsClaudeMidConvoSystemMessages } from "../families/anthropic.ts";
import {
enableOpenAiGrammarTools,
enableOpenAiMidConvoSystemMessages,
openAiStandardCost,
} from "../families/openai.ts";
const BASE_URL = "https://api.individual.githubcopilot.com";
const STATIC_HEADERS = {
"User-Agent": "GitHubCopilotChat/0.35.0",
"Editor-Version": "vscode/1.107.0",
"Editor-Plugin-Version": "copilot-chat/0.35.0",
"Copilot-Integration-Id": "vscode-chat",
} as const;
// GitHub's "Models with extended capabilities" table lists these Copilot models as supporting
// the extended 1 million token context window.
const EXTENDED_CONTEXT_MODEL_IDS = new Set([
"claude-fable-5",
"claude-opus-4.8",
"claude-opus-5",
"claude-opus-5.5",
"claude-sonnet-4.6",
"claude-sonnet-5",
"gpt-5.3-codex",
"gpt-6-astra",
"gpt-6-luna",
"gpt-6-sol",
]);
const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_MODEL_IDS = new Set([
"claude-haiku-4.5",
"claude-sonnet-4",
"claude-sonnet-4.5",
]);
// Checked manually against the authenticated GitHub Copilot /models endpoint on 2026-06-15.
// Keep this to narrow corrections over models.dev metadata instead of snapshotting Copilot's catalog.
const THINKING_LEVEL_OVERRIDES: Record<string, ThinkingLevelMap> = {
"claude-opus-4.8": { minimal: "low" },
"claude-opus-5": { minimal: "low" },
"claude-sonnet-4.6": { minimal: "low", max: "max" },
};
/** Claude 4.x and 5.x use Messages; GPT, Grok, OSWE, and MAI-Code are only served on /responses. */
function copilotApi(modelId: string): Api {
if (/^claude-(haiku|sonnet|opus|fable)-[45]([.-]|$)/.test(modelId)) return "anthropic-messages";
if (/^(gpt-|grok-|oswe|mai-)/.test(modelId)) return "openai-responses";
return "openai-completions";
}
// The authenticated Copilot catalog advertised these models on 2026-09-22,
// but models.dev did not include them yet.
const GPT_6_LUNA: Model<"openai-responses"> = {
id: "gpt-6-luna",
name: "GPT-6 Luna",
api: "openai-responses",
provider: "github-copilot",
baseUrl: BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: openAiStandardCost("gpt-6-luna"),
contextWindow: 1000000,
maxTokens: 128000,
headers: { ...STATIC_HEADERS },
};
export default defineProvider({
id: "github-copilot",
models: (ctx) =>
addMissing(
modelsDevModels(ctx, "github-copilot", {
api: "openai-completions",
baseUrl: BASE_URL,
headers: STATIC_HEADERS,
skipDeprecated: true,
defaultContextWindow: 128000,
defaultMaxTokens: 8192,
map: (model): ChatModel => {
const api = copilotApi(model.id);
const result: ChatModel = { ...model, api };
if (api === "openai-completions") {
result.compat = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
};
}
if (model.id === "claude-opus-5.5") mergeThinking(result, claude55ThinkingLevels(model.id));
return result;
},
}),
[GPT_6_LUNA],
).map((model) => (EXTENDED_CONTEXT_MODEL_IDS.has(model.id) ? { ...model, contextWindow: 1000000 } : model)),
compat: { "openai-completions": { supportsStrictMode: true } },
finalize: (model) => {
if (model.id.startsWith("gpt-5")) mergeThinking(model, { minimal: "low" });
const override = THINKING_LEVEL_OVERRIDES[model.id];
if (override) mergeThinking(model, override);
if (model.api === "anthropic-messages") {
const messagesModel = model as Model<"anthropic-messages">;
// Copilot forwards mid-conversation system messages but rejects
// `tool_addition`/`tool_removal` blocks, so tool changes stay top-level.
if (supportsClaudeMidConvoSystemMessages(model.id)) {
mergeCompat(messagesModel, { supportsMidConvoSystemMessages: true });
}
if (EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_MODEL_IDS.has(model.id)) {
mergeCompat(messagesModel, { supportsEagerToolInputStreaming: false });
}
}
// Copilot forwards Kimi K3's system text but silently drops its tool-bearing message.
if (model.api === "openai-completions" && model.id === "kimi-k3") {
mergeCompat(model as Model<"openai-completions">, { supportsMidConvoSystemMessages: true });
}
// Copilot passes mid-conversation developer messages and `additional_tools`
// through to OpenAI unchanged; tool search is not verified through it.
enableOpenAiMidConvoSystemMessages(model, { proxy: true });
enableOpenAiGrammarTools(model);
},
});

View file

@ -0,0 +1,43 @@
import { defineProvider, modelsDevModels } from "../define.ts";
import { googleThinkingLevels } from "../families/google.ts";
import { modelsDevChatModels } from "../sources/models-dev.ts";
import { GOOGLE_ALIAS_TARGETS } from "./google.ts";
export default defineProvider({
id: "google-vertex",
models: (ctx) => {
const sources = new Map(
modelsDevChatModels(ctx.sources.modelsDev, "google-vertex").map((model) => [model.id, model]),
);
return modelsDevModels(ctx, "google-vertex", {
api: "google-vertex",
baseUrl: "https://{location}-aiplatform.googleapis.com",
recordReasoning: false,
// The models.dev catalog also includes Claude, OpenAI, and other MaaS models
// that do not use the @google/genai Gemini streaming path.
include: (source) => source.id.startsWith("gemini-") && source.id !== "gemini-3.1-flash-lite-preview",
map: (model, aliasSource) => {
const target = GOOGLE_ALIAS_TARGETS[model.id];
const source = (target && sources.get(target)) || aliasSource;
const thinkingLevelMap = googleThinkingLevels(model.id, source.reasoningOptions);
return {
...model,
reasoning: source.reasoning,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
input: source.imageInput ? ["text", "image"] : ["text"],
cost: {
input: source.cost.input || 0,
output: source.cost.output || 0,
// models.dev reports Vertex cache_read/cache_write values for Gemini 2.5 Flash that
// do not match the official Gemini API standard pricing table. pi only accounts
// cachedContentTokenCount as cacheRead.
cacheRead: model.id === "gemini-2.5-flash" ? 0.03 : source.cost.cacheRead || 0,
cacheWrite: 0,
},
contextWindow: source.contextWindow || 4096,
maxTokens: source.maxTokens || 4096,
};
},
});
},
});

View file

@ -0,0 +1,36 @@
import { defineProvider, modelsDevModels, withInputLimits } from "../define.ts";
import { googleThinkingLevels } from "../families/google.ts";
import { modelsDevChatModels, modelsDevCost } from "../sources/models-dev.ts";
// Rolling aliases take their metadata from the model they currently point to.
export const GOOGLE_ALIAS_TARGETS: Record<string, string> = {
"gemini-flash-latest": "gemini-3.5-flash",
"gemini-flash-lite-latest": "gemini-3.1-flash-lite",
};
export default defineProvider({
id: "google",
models: (ctx) => {
const sources = new Map(modelsDevChatModels(ctx.sources.modelsDev, "google").map((model) => [model.id, model]));
return modelsDevModels(ctx, "google", {
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
recordReasoning: false,
map: (model, aliasSource) => {
const target = GOOGLE_ALIAS_TARGETS[model.id];
const source = (target && sources.get(target)) || aliasSource;
const thinkingLevelMap = googleThinkingLevels(model.id, source.reasoningOptions);
return {
...model,
reasoning: source.reasoning,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
input: source.imageInput ? ["text", "image"] : ["text"],
cost: modelsDevCost(source.cost),
contextWindow: source.contextWindow || 4096,
maxTokens: source.maxTokens || 4096,
};
},
});
},
finalize: (model) => withInputLimits(model, { maxRequestBytes: 20 * 1024 * 1024, images: { maxPerRequest: 3600 } }),
});

View file

@ -0,0 +1,13 @@
import { defineProvider, mergeThinking, modelsDevModels } from "../define.ts";
export default defineProvider({
id: "groq",
models: (ctx) =>
modelsDevModels(ctx, "groq", { api: "openai-completions", baseUrl: "https://api.groq.com/openai/v1" }),
compat: { "openai-completions": { supportsStrictMode: true } },
finalize: (model) => {
if (model.id === "qwen/qwen3.6-27b") {
mergeThinking(model, { minimal: null, low: null, medium: null, high: "default" });
}
},
});

View file

@ -0,0 +1,12 @@
import { defineProvider, modelsDevModels } from "../define.ts";
export default defineProvider({
id: "huggingface",
models: (ctx) =>
modelsDevModels(ctx, "huggingface", {
api: "openai-completions",
baseUrl: "https://router.huggingface.co/v1",
compat: { supportsDeveloperRole: false },
}),
compat: { "openai-completions": { supportsStrictMode: true } },
});

View file

@ -0,0 +1,90 @@
// Every built-in provider catalog. Add new providers here.
import type { ProviderDefinition } from "../define.ts";
import amazonBedrock from "./amazon-bedrock.ts";
import antLing from "./ant-ling.ts";
import anthropic from "./anthropic.ts";
import azure from "./azure.ts";
import baseten from "./baseten.ts";
import cerebras from "./cerebras.ts";
import cloudflareAiGateway from "./cloudflare-ai-gateway.ts";
import cloudflareWorkersAi from "./cloudflare-workers-ai.ts";
import deepseek from "./deepseek.ts";
import fireworks from "./fireworks.ts";
import githubCopilot from "./github-copilot.ts";
import google from "./google.ts";
import googleVertex from "./google-vertex.ts";
import groq from "./groq.ts";
import huggingface from "./huggingface.ts";
import kimiCoding from "./kimi-coding.ts";
import meta from "./meta.ts";
import minimax from "./minimax.ts";
import minimaxCn from "./minimax-cn.ts";
import mistral from "./mistral.ts";
import moonshotai from "./moonshotai.ts";
import moonshotaiCn from "./moonshotai-cn.ts";
import nvidia from "./nvidia.ts";
import openai from "./openai.ts";
import openaiCodex from "./openai-codex.ts";
import opencode from "./opencode.ts";
import opencodeGo from "./opencode-go.ts";
import openrouter from "./openrouter.ts";
import qwenTokenPlan from "./qwen-token-plan.ts";
import qwenTokenPlanCn from "./qwen-token-plan-cn.ts";
import qwenTokenPlanIndividual from "./qwen-token-plan-individual.ts";
import radius from "./radius.ts";
import together from "./together.ts";
import typesafe from "./typesafe.ts";
import vercelAiGateway from "./vercel-ai-gateway.ts";
import xai from "./xai.ts";
import xiaomi from "./xiaomi.ts";
import xiaomiTokenPlanAms from "./xiaomi-token-plan-ams.ts";
import xiaomiTokenPlanCn from "./xiaomi-token-plan-cn.ts";
import xiaomiTokenPlanSgp from "./xiaomi-token-plan-sgp.ts";
import zai from "./zai.ts";
import zaiCodingCn from "./zai-coding-cn.ts";
export const PROVIDERS: readonly ProviderDefinition[] = [
amazonBedrock,
antLing,
anthropic,
azure,
baseten,
cerebras,
cloudflareAiGateway,
cloudflareWorkersAi,
deepseek,
fireworks,
githubCopilot,
googleVertex,
google,
groq,
huggingface,
kimiCoding,
meta,
minimaxCn,
minimax,
mistral,
moonshotaiCn,
moonshotai,
nvidia,
openaiCodex,
openai,
opencodeGo,
opencode,
openrouter,
qwenTokenPlanCn,
qwenTokenPlanIndividual,
qwenTokenPlan,
radius,
together,
typesafe,
vercelAiGateway,
xai,
xiaomiTokenPlanAms,
xiaomiTokenPlanCn,
xiaomiTokenPlanSgp,
xiaomi,
zaiCodingCn,
zai,
];

View file

@ -0,0 +1,55 @@
import type { Model, ModelCost } from "../../src/types.ts";
import { defineProvider, modelsDevModels } from "../define.ts";
import { KIMI_K3_COST, KIMI_K3_OPENAI_COMPAT, KIMI_OPENAI_COMPAT } from "../families/kimi.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
// Kimi Coding is subscription-backed, so models.dev reports zero cost. Use the
// equivalent Moonshot API rates to estimate the value of subscription usage.
const IMPLIED_COSTS: Record<string, ModelCost> = {
k3: KIMI_K3_COST,
"k3-256k": KIMI_K3_COST,
"k3.1-pro": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 0 },
"k3.1-pro-highspeed": { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 0 },
"kimi-for-coding": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
"kimi-for-coding-highspeed": { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 0 },
};
// Kimi Coding uses Chat Completions, the same protocol as the official Kimi Code client.
export default defineProvider({
id: "kimi-coding",
models: (ctx) =>
modelsDevModels(ctx, "kimi-code-plan-global", {
api: "openai-completions",
baseUrl: "https://api.kimi.com/coding/v1",
recordReasoning: false,
map: (model, source): Model<"openai-completions"> => {
// K3-style models report effort levels and dynamic tool loading (tools declared in
// later system messages) in https://api.kimi.com/coding/v1/models; models.dev mirrors
// the effort levels. kimi-for-coding-highspeed reports neither and rejects
// tool-bearing system messages with a 400.
const isK3Style = source.reasoningOptions?.some((option) => option.type === "effort") === true;
const impliedCost = IMPLIED_COSTS[model.id];
// K3-style models send reasoning_effort, so expose the effort values models.dev lists.
const effortLevels =
isK3Style && source.reasoningOptions ? getEffortThinkingLevelMap(source.reasoningOptions) : undefined;
return {
...model,
compat: {
...(isK3Style ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT),
// All models accept system text after the conversation starts.
supportsMidConvoSystemMessages: true,
...(isK3Style ? { supportsMidConvoToolAdditions: true } : {}),
},
cost: {
input: source.cost.input || impliedCost?.input || 0,
output: source.cost.output || impliedCost?.output || 0,
cacheRead: source.cost.cacheRead || impliedCost?.cacheRead || 0,
cacheWrite: source.cost.cacheWrite || impliedCost?.cacheWrite || 0,
},
// Kimi Code reports every model as always-thinking.
thinkingLevelMap: { ...effortLevels, off: null },
};
},
}),
compat: { "openai-completions": { supportsStrictMode: true } },
});

View file

@ -0,0 +1,6 @@
import { defineProvider, modelsDevModels } from "../define.ts";
export default defineProvider({
id: "meta",
models: (ctx) => modelsDevModels(ctx, "meta", { api: "openai-responses", baseUrl: "https://api.meta.ai/v1" }),
});

View file

@ -0,0 +1,7 @@
import { defineProvider } from "../define.ts";
import { minimaxModels } from "./minimax.ts";
export default defineProvider({
id: "minimax-cn",
models: (ctx) => minimaxModels(ctx, "minimax-cn", "https://api.minimaxi.com/anthropic"),
});

View file

@ -0,0 +1,26 @@
import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
// MiniMax asked to hide older models from its direct providers (#2596).
const HIDDEN_MODEL_IDS = new Set(["MiniMax-M2", "MiniMax-M2.1", "MiniMax-M2.5", "MiniMax-M2.5-highspeed"]);
/** MiniMax's Anthropic-compatible API. The SDK appends /v1/messages. */
export function minimaxModels(ctx: ProviderContext, source: string, baseUrl: string) {
return modelsDevModels(ctx, source, {
api: "anthropic-messages",
baseUrl,
recordReasoning: false,
include: (model) => !HIDDEN_MODEL_IDS.has(model.id),
map: (model, source) => {
// Models with effort levels (e.g. MiniMax-M3.1-Flash) take adaptive thinking with
// output_config.effort and reject thinking.type="disabled" (no "none" effort, so off is null).
const thinkingLevelMap = getEffortThinkingLevelMap(source.reasoningOptions ?? []);
return thinkingLevelMap ? { ...model, thinkingLevelMap, compat: { forceAdaptiveThinking: true } } : model;
},
});
}
export default defineProvider({
id: "minimax",
models: (ctx) => minimaxModels(ctx, "minimax", "https://api.minimax.io/anthropic"),
});

View file

@ -0,0 +1,46 @@
import { addMissing, defineProvider, modelsDevModels } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
import { roundCost } from "../sources/models-dev.ts";
export default defineProvider({
id: "mistral",
models: (ctx) =>
addMissing(
modelsDevModels(ctx, "mistral", {
api: "mistral-conversations",
baseUrl: "https://api.mistral.ai",
recordReasoning: false,
map: (model, source) => {
// Models with effort values use `reasoning_effort` with these levels.
// Reasoning models without them (Magistral) use `prompt_mode`.
const thinkingLevelMap = getEffortThinkingLevelMap(source.reasoningOptions ?? []);
return {
...model,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
cost: {
input: source.cost.input || 0,
output: source.cost.output || 0,
// Mistral bills cached input at 10% when models.dev does not say otherwise.
cacheRead: source.cost.cacheRead ?? (source.cost.input ? roundCost(source.cost.input * 0.1) : 0),
cacheWrite: source.cost.cacheWrite || 0,
},
};
},
}),
[
{
id: "mistral-medium-3.5",
name: "Mistral Medium 3.5",
api: "mistral-conversations",
provider: "mistral",
baseUrl: "https://api.mistral.ai",
reasoning: true,
thinkingLevelMap: getEffortThinkingLevelMap([{ type: "effort", values: ["none", "high"] }]),
input: ["text", "image"],
cost: { input: 1.5, output: 7.5, cacheRead: 0, cacheWrite: 0 },
contextWindow: 262144,
maxTokens: 262144,
},
],
),
});

View file

@ -0,0 +1,9 @@
import { defineProvider } from "../define.ts";
import { finalizeMoonshotModel, MOONSHOT_COMPLETIONS_COMPAT, moonshotModels } from "./moonshotai.ts";
export default defineProvider({
id: "moonshotai-cn",
models: (ctx) => moonshotModels(ctx, "moonshotai-cn", "https://api.moonshot.cn/v1"),
compat: { "openai-completions": MOONSHOT_COMPLETIONS_COMPAT },
finalize: finalizeMoonshotModel,
});

View file

@ -0,0 +1,71 @@
import type { Model } from "../../src/types.ts";
import {
type ChatModel,
defineProvider,
mergeCompat,
mergeThinking,
modelsDevModels,
type ProviderContext,
} from "../define.ts";
import { isKimiK3FamilyId, KIMI_K3_COST, KIMI_K3_OPENAI_COMPAT, KIMI_OPENAI_COMPAT } from "../families/kimi.ts";
const ALWAYS_THINKING_MODEL_IDS = new Set(["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]);
// Kimi K2.6/K2.7 accept system text after the conversation starts but reject
// tool-bearing system messages.
const TEXT_ONLY_MID_CONVO_MODEL_IDS = new Set(["kimi-k2.6", ...ALWAYS_THINKING_MODEL_IDS]);
export function moonshotModels(ctx: ProviderContext, source: string, baseUrl: string) {
return modelsDevModels(ctx, source, {
api: "openai-completions",
baseUrl,
map: (model, source): Model<"openai-completions"> => {
const isKimiK3 = isKimiK3FamilyId(model.id);
return {
...model,
reasoning: isKimiK3 || source.reasoning,
// Moonshot does not bill cache writes; models.dev lists the input rate as cache_write for the K3 family.
cost:
model.id === "kimi-k3"
? { ...KIMI_K3_COST }
: {
input: source.cost.input || 0,
output: source.cost.output || 0,
cacheRead: source.cost.cacheRead || 0,
cacheWrite: isKimiK3 ? 0 : source.cost.cacheWrite || 0,
},
compat: { ...(isKimiK3 ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT) },
};
},
});
}
export const MOONSHOT_COMPLETIONS_COMPAT = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
} as const;
export function finalizeMoonshotModel(model: ChatModel): void {
if (ALWAYS_THINKING_MODEL_IDS.has(model.id)) {
// Kimi K2.7 Code is always-thinking. Official docs say
// `thinking: { type: "disabled" }` is rejected, and callers can omit
// the thinking parameter to use the enabled default.
mergeThinking(model, { off: null });
}
const isKimiK3 = isKimiK3FamilyId(model.id);
if (isKimiK3 || TEXT_ONLY_MID_CONVO_MODEL_IDS.has(model.id)) {
mergeCompat(model as Model<"openai-completions">, {
supportsMidConvoSystemMessages: true,
// The Kimi K3 family also accepts tool-bearing system messages.
...(isKimiK3 ? { supportsMidConvoToolAdditions: true } : {}),
});
}
}
export default defineProvider({
id: "moonshotai",
models: (ctx) => moonshotModels(ctx, "moonshotai", "https://api.moonshot.ai/v1"),
compat: { "openai-completions": MOONSHOT_COMPLETIONS_COMPAT },
finalize: finalizeMoonshotModel,
});

View file

@ -0,0 +1,64 @@
import { defineProvider, modelsDevModels } from "../define.ts";
import { NVIDIA_BASE_URL, normalizeNvidiaModelId } from "../sources/index.ts";
// Listed by NIM but unusable through pi (no tool calls, broken streaming, or retired).
const UNSUPPORTED_MODEL_IDS = new Set([
"abacusai/dracarys-llama-3.1-70b-instruct",
"bytedance/seed-oss-36b-instruct",
"deepseek-ai/deepseek-v4-flash",
"deepseek-ai/deepseek-v4-pro",
"google/gemma-2-2b-it",
"google/gemma-3n-e2b-it",
"google/gemma-3n-e4b-it",
"google/gemma-4-31b-it",
"meta/llama-3.2-1b-instruct",
"meta/llama-4-maverick-17b-128e-instruct",
"microsoft/phi-4-mini-instruct",
"minimaxai/minimax-m2.7",
"mistralai/mistral-nemotron",
"nvidia/nemotron-mini-4b-instruct",
"qwen/qwen3-next-80b-a3b-instruct",
"qwen/qwen3.5-397b-a17b",
"sarvamai/sarvam-m",
"upstage/solar-10.7b-instruct",
]);
export default defineProvider({
id: "nvidia",
models: (ctx) => {
const liveIds = ctx.sources.nvidiaModelIds;
const liveId = (id: string) => liveIds.get(id) ?? liveIds.get(normalizeNvidiaModelId(id));
return modelsDevModels(ctx, "nvidia", {
api: "openai-completions",
baseUrl: NVIDIA_BASE_URL,
headers: { "NVCF-POLL-SECONDS": "3600" },
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsStrictMode: false,
supportsLongCacheRetention: false,
},
// Only models the live NIM API serves, under the id it uses.
include: (source) => {
if (!source.textInput || !source.textOutput) return false;
const id = liveId(source.id);
return id !== undefined && !UNSUPPORTED_MODEL_IDS.has(id);
},
map: (model) => {
const id = liveId(model.id) ?? model.id;
return { ...model, id, name: model.name === model.id ? id : model.name };
},
});
},
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsLongCacheRetention: false,
},
},
});

View file

@ -0,0 +1,68 @@
import type { Model } from "../../src/types.ts";
import { defineProvider, mergeCompat, mergeThinking } from "../define.ts";
import {
enableOpenAiGrammarTools,
enableOpenAiMidConvoSystemMessages,
OPENAI_TOOL_SEARCH_MODEL_IDS,
openAiStandardCost,
supportsOpenAiXhigh,
} from "../families/openai.ts";
// ChatGPT OAuth models. models.dev does not list them; keep a small, explicit
// list without aliases. Only list models the Codex backend accepts for ChatGPT
// accounts: GPT-5.5 and GPT-5.3 Codex Spark are rejected with "not supported
// when using Codex with a ChatGPT account".
const MODELS = [
// GPT-6.1 uses the 500k default returned by /backend-api/codex/models.
{ id: "gpt-6.1-sol", name: "GPT-6.1 Sol", contextWindow: 500000 },
{ id: "gpt-6.1-terra", name: "GPT-6.1 Terra", contextWindow: 500000 },
// GPT-5.6 and GPT-6 use Codex's 272k default catalog limit.
{ id: "gpt-6-astra", name: "GPT-6 Astra", contextWindow: 272000 },
{ id: "gpt-6-sol", name: "GPT-6 Sol", contextWindow: 272000 },
{ id: "gpt-6-luna", name: "GPT-6 Luna", contextWindow: 272000 },
{ id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextWindow: 272000 },
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextWindow: 272000 },
{ id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextWindow: 272000 },
];
// Codex CLI keeps additional_tools behind its own model list.
const ADDITIONAL_TOOLS_MODEL_IDS = new Set([
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
export default defineProvider({
id: "openai-codex",
models: () =>
MODELS.map(
({ id, name, contextWindow }): Model<"openai-codex-responses"> => ({
id,
name,
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text", "image"],
cost: openAiStandardCost(id),
contextWindow,
maxTokens: 128000,
}),
),
finalize: (model) => {
if (supportsOpenAiXhigh(model.id)) mergeThinking(model, { minimal: "low" });
enableOpenAiGrammarTools(model);
if (OPENAI_TOOL_SEARCH_MODEL_IDS.has(model.id)) {
mergeCompat(model as Model<"openai-codex-responses">, {
...(ADDITIONAL_TOOLS_MODEL_IDS.has(model.id) ? { supportsAdditionalTools: true } : {}),
supportsToolSearch: true,
});
}
enableOpenAiMidConvoSystemMessages(model, { proxy: false });
},
});

View file

@ -0,0 +1,141 @@
import type { ClassifierModel, Model } from "../../src/types.ts";
import {
addMissing,
type ChatModel,
defineProvider,
mergeCompat,
mergeThinking,
modelsDevModels,
withInputLimits,
} from "../define.ts";
import {
enableOpenAiGrammarTools,
enableOpenAiMidConvoSystemMessages,
OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
OPENAI_STANDARD_COSTS,
OPENAI_TOOL_SEARCH_MODEL_IDS,
withOpenAiLongContextPricing,
} from "../families/openai.ts";
const BASE_URL = "https://api.openai.com/v1";
// models.dev lists this alias, but it is not accepted by OpenAI APIs.
const UNSUPPORTED_MODEL_IDS = new Set(["gpt-5.6"]);
// Keep direct OpenAI requests in the short-context pricing tier by default. Users can opt into the
// larger context through model overrides, so retain long-context cost metadata on the capped models.
const SHORT_CONTEXT_CAPPED_MODEL_IDS = new Set([
"gpt-5.4",
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-6.1-sol",
"gpt-6.1-terra",
]);
const LONG_CONTEXT_PRICING_MODEL_IDS = new Set([...SHORT_CONTEXT_CAPPED_MODEL_IDS, "gpt-5.4-pro", "gpt-5.5-pro"]);
const NONE_REASONING_MODEL_IDS = new Set([
"gpt-5.1",
"gpt-5.2",
"gpt-5.3-codex",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-sol",
"gpt-6-luna",
]);
// OpenAI documents configuration_update for the GPT-6 family.
// https://developers.openai.com/api/docs/guides/reasoning
const MID_CONVO_EFFORT_MODEL_IDS = new Set(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-6.1-terra"]);
// Not listed by models.dev.
const GPT_5_CHAT_LATEST: Model<"openai-responses"> = {
id: "gpt-5-chat-latest",
name: "GPT-5 Chat Latest",
api: "openai-responses",
baseUrl: BASE_URL,
provider: "openai",
reasoning: false,
input: ["text", "image"],
cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
};
// OpenAI Decisions API (public beta): gpt-6-luna is its only model. It bills input tokens only,
// with the same long-context multiplier as chat requests. Only API keys work: Sign in with ChatGPT
// tokens are rejected on /v1/decisions, and the Codex backend has no Decisions route.
// The endpoint rejects inputs above 922K tokens (the model's documented maximum input), but
// requests running longer than about five seconds, currently above roughly 600K input tokens,
// fail with a gateway timeout.
// https://developers.openai.com/api/docs/guides/decisions
const CLASSIFIER_MODELS: ClassifierModel<"openai-decisions">[] = [
{
type: "classifier",
id: "gpt-6-luna",
name: "GPT-6 Luna",
api: "openai-decisions",
provider: "openai",
baseUrl: BASE_URL,
input: ["text", "image"],
cost: withOpenAiLongContextPricing({ input: 0.1, output: 0, cacheRead: 0, cacheWrite: 0 }),
contextWindow: 922000,
},
];
function correctUpstream(model: ChatModel): ChatModel {
if (SHORT_CONTEXT_CAPPED_MODEL_IDS.has(model.id)) {
model.contextWindow = OPENAI_LONG_CONTEXT_INPUT_THRESHOLD;
model.maxTokens = 128000;
}
if (LONG_CONTEXT_PRICING_MODEL_IDS.has(model.id)) {
model.cost = withOpenAiLongContextPricing(OPENAI_STANDARD_COSTS[model.id] ?? model.cost);
}
// models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit);
// the actual max output is 128000. Also propagates to the derived Azure clone.
if (model.id === "gpt-5-pro") model.maxTokens = 128000;
return model;
}
export default defineProvider({
id: "openai",
models: (ctx) =>
addMissing(
modelsDevModels(ctx, "openai", {
api: "openai-responses",
baseUrl: BASE_URL,
// Audio models are only served on Chat Completions.
protocol: "responses",
include: (source) => !UNSUPPORTED_MODEL_IDS.has(source.id),
map: correctUpstream,
}),
[GPT_5_CHAT_LATEST],
),
classifiers: () => CLASSIFIER_MODELS,
finalize: (model) => {
if (model.api !== "openai-responses") return;
const responsesModel = model as Model<"openai-responses">;
if (NONE_REASONING_MODEL_IDS.has(model.id)) mergeThinking(model, { off: "none" });
if (model.id === "gpt-5.5") mergeThinking(model, { minimal: null });
mergeCompat(responsesModel, { supportsStrictMode: true });
enableOpenAiGrammarTools(model);
if (OPENAI_TOOL_SEARCH_MODEL_IDS.has(model.id)) {
mergeCompat(responsesModel, { supportsAdditionalTools: true, supportsToolSearch: true });
}
enableOpenAiMidConvoSystemMessages(model, { proxy: false });
// OpenAI charges prompt-cache writes starting with the GPT-5.6 family, and exactly
// those models accept `prompt_cache_options`; older models reject the parameter.
// https://developers.openai.com/api/docs/guides/prompt-caching
if (model.cost.cacheWrite > 0) mergeCompat(responsesModel, { supportsExplicitPromptCacheMode: true });
if (MID_CONVO_EFFORT_MODEL_IDS.has(model.id)) mergeCompat(responsesModel, { supportsMidConvoEffort: true });
withInputLimits(model, { maxRequestBytes: 512 * 1024 * 1024, images: { maxPerRequest: 1500 } });
},
});

View file

@ -0,0 +1,9 @@
import { defineProvider } from "../define.ts";
import { finalizeOpencodeModel, OPENCODE_COMPLETIONS_COMPAT, opencodeModels } from "./opencode.ts";
export default defineProvider({
id: "opencode-go",
models: opencodeModels,
compat: { "openai-completions": OPENCODE_COMPLETIONS_COMPAT },
finalize: finalizeOpencodeModel,
});

View file

@ -0,0 +1,206 @@
import type { Api, ClassifierModel, Model, OpenAICompletionsCompat, OpenAIResponsesCompat } from "../../src/types.ts";
import {
type ChatModel,
defineProvider,
mergeCompat,
mergeThinking,
modelsDevModels,
type ProviderContext,
} from "../define.ts";
import { supportsClaudeMidConvoSystemMessages } from "../families/anthropic.ts";
import { googleThinkingLevels } from "../families/google.ts";
import { isKimiK3FamilyId } from "../families/kimi.ts";
import { enableOpenAiGrammarTools, enableOpenAiMidConvoSystemMessages } from "../families/openai.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
import type { ModelsDevModel } from "../sources/models-dev.ts";
// OpenAI-compatible endpoints reject the long prompt-cache retention parameter for these models.
const LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([
"opencode:deepseek-v4-flash",
"opencode:deepseek-v4.1-flash",
"opencode:deepseek-v4-pro",
"opencode:kimi-k2.5",
"opencode:kimi-k2.6",
"opencode:minimax-m2.7",
"opencode-go:kimi-k2.6",
]);
// OpenCode serves each model on one protocol-specific endpoint. Native protocols
// come before generic Chat Completions if models.dev ever lists several.
const OPENCODE_PROTOCOLS = ["responses", "messages", "generate-content", "chat-completions"] as const;
interface OpencodeApi {
api: Api;
baseUrl: string;
compat?: OpenAICompletionsCompat | OpenAIResponsesCompat;
}
/**
* The API and base URL models.dev lists for a model. Models served only on a
* protocol pi does not support fail strict generation and are skipped otherwise.
*/
function opencodeApi(ctx: ProviderContext, source: ModelsDevModel): OpencodeApi | undefined {
const entry = OPENCODE_PROTOCOLS.map((protocol) =>
source.apis.find((candidate) => candidate.protocol === protocol),
).find((candidate) => candidate !== undefined);
if (!entry) {
const protocols = source.apis.map((candidate) => candidate.protocol).join(", ") || "none";
const message = `models.dev lists no supported API for ${ctx.providerId}/${source.id} (${protocols})`;
if (ctx.strict) throw new Error(message);
console.warn(`Skipping model: ${message}`);
return undefined;
}
switch (entry.protocol) {
case "responses":
return {
api: "openai-responses",
baseUrl: entry.baseUrl,
compat: { sessionAffinityFormat: "openai-nosession" },
};
case "messages":
// The Anthropic SDK appends /v1/messages itself.
return { api: "anthropic-messages", baseUrl: entry.baseUrl.replace(/\/v1\/?$/, "") };
case "generate-content":
return { api: "google-generative-ai", baseUrl: entry.baseUrl };
case "chat-completions":
return {
api: "openai-completions",
baseUrl: entry.baseUrl,
...(entry.aisdk === "@ai-sdk/alibaba" ? { compat: { cacheControlFormat: "anthropic" } } : {}),
};
}
}
/** OpenCode Zen and Go models, on the endpoints models.dev lists for them. */
export function opencodeModels(ctx: ProviderContext): ChatModel[] {
const provider = ctx.providerId;
return modelsDevModels(ctx, provider, {
api: "openai-completions",
// Replaced by the base URL of the API models.dev lists for each model.
baseUrl: "",
skipDeprecated: true,
// Codex Spark is not usable through OpenCode.
include: (source) => source.id !== "gpt-5.3-codex-spark",
map: (model, source) => {
const served = opencodeApi(ctx, source);
if (!served) return undefined;
let { api, baseUrl, compat } = served;
if (provider === "opencode" && model.id === "grok-build-0.1") {
compat = { ...compat, supportsReasoningEffort: false };
}
if (model.id === "kimi-k2.6") {
// OpenCode Kimi K2.6 accepts Anthropic-style thinking objects
// and rejects string thinking values or combined reasoning_effort.
compat = { ...compat, thinkingFormat: "deepseek", supportsReasoningEffort: false };
}
// models.dev reports these as @ai-sdk/anthropic, but OpenCode Go serves
// them through the OpenAI-compatible /openai/v1/chat/completions path.
if (
provider === "opencode-go" &&
api === "anthropic-messages" &&
(model.id === "qwen3.5-plus" || model.id === "qwen3.6-plus")
) {
api = "openai-completions";
baseUrl = baseUrl.replace(/\/anthropic$/, "/openai/v1");
// Qwen/DashScope uses enable_thinking at the top level.
compat = { ...compat, thinkingFormat: "qwen" };
}
if (api === "openai-completions") {
compat = {
...compat,
maxTokensField: "max_tokens",
...(LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has(`${provider}:${model.id}`)
? { supportsLongCacheRetention: false }
: {}),
};
}
const thinkingLevelMap =
api === "google-generative-ai"
? googleThinkingLevels(model.id, source.reasoningOptions)
: provider === "opencode-go" && model.id === "deepseek-v4.1-flash"
? getEffortThinkingLevelMap(source.reasoningOptions ?? [])
: undefined;
const result: ChatModel = { ...model, api, baseUrl, ...(thinkingLevelMap ? { thinkingLevelMap } : {}) };
if (compat) result.compat = compat;
// OpenCode variants list Claude Sonnet 4/4.5 with 1M context, actual limit is 200K.
if (model.id === "claude-sonnet-4-5" || model.id === "claude-sonnet-4") result.contextWindow = 200000;
if (["claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4.6", "claude-sonnet-4.6"].includes(model.id)) {
result.contextWindow = 1000000;
}
if (model.id === "gpt-5.4") {
result.contextWindow = 272000;
result.maxTokens = 128000;
}
return result;
},
});
}
export const OPENCODE_COMPLETIONS_COMPAT = {
supportsStore: false,
supportsDeveloperRole: false,
supportsStrictMode: true,
} as const;
/** Behavior shared by OpenCode Zen and Go. */
export function finalizeOpencodeModel(model: ChatModel): void {
if (model.provider === "opencode-go" && model.id === "glm-5.2") {
mergeThinking(model, { off: null, minimal: null, low: null, medium: null, high: "high", max: "max" });
}
if (model.provider === "opencode-go" && model.id === "kimi-k2.6") {
// OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers.
mergeThinking(model, { minimal: null, low: null, medium: null });
}
if (model.provider === "opencode" && model.id === "grok-build-0.1") {
// OpenCode Zen Grok Build reasons by default but rejects explicit reasoningEffort.
mergeThinking(model, { off: null, minimal: null, low: null, medium: null });
}
if (model.api === "anthropic-messages") {
const messagesModel = model as Model<"anthropic-messages">;
// OpenCode Zen forwards mid-conversation system messages but rejects
// `tool_addition`/`tool_removal` blocks, so tool changes stay top-level.
if (model.provider === "opencode" && supportsClaudeMidConvoSystemMessages(model.id)) {
mergeCompat(messagesModel, { supportsMidConvoSystemMessages: true });
}
// OpenCode Qwen 3.8 Flash emits and accepts thinking blocks with empty signatures.
if (model.id === "qwen3.8-flash") mergeCompat(messagesModel, { allowEmptySignature: true });
}
// The Kimi K3 family accepts tool-bearing system messages; OpenCode passes them through.
if (model.api === "openai-completions" && isKimiK3FamilyId(model.id)) {
mergeCompat(model as Model<"openai-completions">, {
supportsMidConvoSystemMessages: true,
supportsMidConvoToolAdditions: true,
});
}
// OpenCode passes mid-conversation developer messages and `additional_tools`
// through to OpenAI unchanged; tool search is not verified through it.
enableOpenAiMidConvoSystemMessages(model, { proxy: true });
}
// OpenCode Zen serves Jev through its TypeSafe-compatible System One endpoint.
// Neither its /inference/openai/v1/models listing nor models.dev carries metadata for it.
// https://opencode.ai/docs/zen
function jev(id: string, name: string, input: number): ClassifierModel<"typesafe-system-one"> {
return {
type: "classifier",
id,
name,
api: "typesafe-system-one",
provider: "opencode",
baseUrl: "https://opencode.ai/inference/systemone/v1",
input: ["text"],
cost: { input, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32000,
};
}
export default defineProvider({
id: "opencode",
models: opencodeModels,
classifiers: () => [jev("jev-1.13", "Jev 1.13", 0.042), jev("jev-1.13-free", "Jev 1.13 Free", 0)],
compat: { "openai-completions": OPENCODE_COMPLETIONS_COMPAT },
finalize: (model) => {
finalizeOpencodeModel(model);
enableOpenAiGrammarTools(model);
},
});

View file

@ -0,0 +1,108 @@
import type { Model, OpenAICompletionsCompat } from "../../src/types.ts";
import { addMissing, type ChatModel, defineProvider, mergeCompat, mergeThinking } from "../define.ts";
import { enableClaudeMidConvoEffort, supportsClaudeMidConvoEffort } from "../families/anthropic.ts";
import { isKimiK3FamilyId, KIMI_K3_MAX_TOKENS } from "../families/kimi.ts";
import { OPENAI_MID_CONVO_MODEL_IDS } from "../families/openai.ts";
const BASE_URL = "https://openrouter.ai/api/v1";
// OpenRouter rejects `configuration_update` system messages on Opus 5 ("Mid-conversation
// reasoning effort (configuration_update) is not supported on anthropic/claude-opus-5-20260723")
// while accepting them on Fable 5.1.
const MID_CONVO_EFFORT_UNSUPPORTED_MODEL_IDS = new Set(["anthropic/claude-opus-5"]);
/** Keep selected model metadata stable until upstream settles. */
function correctUpstream(model: ChatModel): ChatModel {
// Keep Kimi K3's canonical output limit when gateway metadata is missing or incorrect.
if (
model.id === "~moonshotai/kimi-latest" ||
(model.id.startsWith("moonshotai/") && isKimiK3FamilyId(model.id.slice("moonshotai/".length)))
) {
model.maxTokens = KIMI_K3_MAX_TOKENS;
}
if (model.id === "moonshotai/kimi-k2.5") {
model.cost = { ...model.cost, input: 0.41, output: 2.06, cacheRead: 0.07 };
model.maxTokens = 4096;
}
if (model.id.startsWith("moonshotai/kimi-k2.6")) {
mergeCompat(model as Model<"openai-completions">, {
supportsDeveloperRole: false,
requiresReasoningContentOnAssistantMessages: true,
});
}
if (model.id === "z-ai/glm-5") model.cost = { ...model.cost, input: 0.6, output: 1.9, cacheRead: 0.119 };
return model;
}
// Router aliases. Their price depends on the model they route to, so it is unknown here.
const ROUTER_MODELS: Model<"openai-completions">[] = [
{
id: "auto",
name: "Auto",
api: "openai-completions",
provider: "openrouter",
baseUrl: BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 2000000,
maxTokens: 30000,
},
// OpenRouter exposes Fusion as a router alias/plugin entry point; its model
// metadata does not advertise tools, but the alias resolves to a concrete model
// that can invoke caller tools and has the openrouter:fusion server tool auto-injected.
{
id: "openrouter/fusion",
name: "OpenRouter: Fusion",
api: "openai-completions",
provider: "openrouter",
baseUrl: BASE_URL,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 30000,
},
];
export default defineProvider({
id: "openrouter",
models: (ctx) =>
addMissing(structuredClone(ctx.sources.openRouter.chat).map(correctUpstream), structuredClone(ROUTER_MODELS)),
images: (ctx) => structuredClone(ctx.sources.openRouter.images),
classifiers: (ctx) => structuredClone(ctx.sources.openRouter.classifiers),
compat: {
"openai-completions": (model): OpenAICompletionsCompat => ({
// OpenRouter passes developer messages through to Anthropic and OpenAI models only.
...(/^(anthropic|openai)\//.test(model.id) ? {} : { supportsDeveloperRole: false }),
thinkingFormat: "openrouter",
supportsStrictMode: true,
...(/^~?anthropic\//.test(model.id) ? { cacheControlFormat: "anthropic" } : {}),
sendSessionAffinityHeaders: true,
}),
},
finalize: (model) => {
if (
model.api === "anthropic-messages" &&
supportsClaudeMidConvoEffort(model.id) &&
!MID_CONVO_EFFORT_UNSUPPORTED_MODEL_IDS.has(model.id)
) {
enableClaudeMidConvoEffort(model as Model<"anthropic-messages">);
}
if (model.id.startsWith("inception/mercury-2")) {
// Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling.
// Mark "off" unsupported so the openai-completions provider omits the reasoning param
// instead of defaulting to {reasoning:{effort:"none"}}.
// Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary.
mergeThinking(model, { off: null });
}
if (model.id === "z-ai/glm-5.2") mergeThinking(model, { xhigh: "xhigh" });
// OpenAI models behind OpenRouter accept plain system text in place.
if (
model.api === "openai-completions" &&
model.id.startsWith("openai/") &&
OPENAI_MID_CONVO_MODEL_IDS.has(model.id.slice("openai/".length))
) {
mergeCompat(model as Model<"openai-completions">, { supportsMidConvoSystemMessages: true });
}
},
});

View file

@ -0,0 +1,13 @@
import { defineProvider } from "../define.ts";
import { QWEN_COMPLETIONS_COMPAT, qwenTokenPlanModels } from "./qwen-token-plan.ts";
export default defineProvider({
id: "qwen-token-plan-cn",
models: (ctx) =>
qwenTokenPlanModels(
ctx,
"alibaba-token-plan-cn",
"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
),
compat: { "openai-completions": QWEN_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,34 @@
import { assertExactModelIds } from "../../scripts/model-data.ts";
import { defineProvider } from "../define.ts";
import { INTERNATIONAL_BASE_URL, QWEN_COMPLETIONS_COMPAT, qwenTokenPlanModels } from "./qwen-token-plan.ts";
// QwenCloud Token Plan Individual text-model allowlist, verified 2026-09-03.
// It shares the international endpoint with a narrower catalog.
// https://docs.qwencloud.com/token-plan/personal/token-plan-personal-overview
const MODEL_IDS = new Set<string>([
"deepseek-v4-flash-0731",
"deepseek-v4-pro",
"deepseek-v4-pro-0813",
"glm-5.2",
"qwen3.6-flash",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-flash",
"qwen3.8-max",
]);
export default defineProvider({
id: "qwen-token-plan-individual",
models: (ctx) => {
const models = qwenTokenPlanModels(ctx, "alibaba-token-plan", INTERNATIONAL_BASE_URL, MODEL_IDS);
if (ctx.strict) {
assertExactModelIds(
ctx.providerId,
MODEL_IDS,
models.map((model) => model.id),
);
}
return models;
},
compat: { "openai-completions": QWEN_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,53 @@
import type { Model, OpenAICompletionsCompat } from "../../src/types.ts";
import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
// Alibaba Cloud Model Studio Token Plan. International and China use separate
// endpoints and API keys (sk-sp- prefix). models.dev lists them as
// "alibaba-token-plan[-cn]".
export const INTERNATIONAL_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
const COMPAT: OpenAICompletionsCompat = {
thinkingFormat: "qwen",
supportsDeveloperRole: false,
supportsStore: false,
supportsReasoningEffort: true,
};
const FALLBACK_THINKING_LEVELS = {
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: "max",
} as const;
const REASONING_EFFORT_FALLBACK_MODEL_IDS = new Set(["glm-5", "glm-5.1"]);
// Retired preview id — models.dev may still list it after GA ships.
const EXCLUDED_MODEL_IDS = new Set(["qwen3.8-max-preview"]);
export function qwenTokenPlanModels(ctx: ProviderContext, source: string, baseUrl: string, modelIds?: Set<string>) {
return modelsDevModels(ctx, source, {
api: "openai-completions",
baseUrl,
recordReasoning: false,
include: (model) => !EXCLUDED_MODEL_IDS.has(model.id) && (!modelIds || modelIds.has(model.id)),
map: (model, source): Model<"openai-completions"> => {
const thinkingLevelMap =
getEffortThinkingLevelMap(source.reasoningOptions ?? []) ??
(REASONING_EFFORT_FALLBACK_MODEL_IDS.has(model.id) ? FALLBACK_THINKING_LEVELS : undefined);
return {
...model,
compat: thinkingLevelMap ? COMPAT : { ...COMPAT, supportsReasoningEffort: false },
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
};
},
});
}
export const QWEN_COMPLETIONS_COMPAT = { supportsStrictMode: true } as const;
export default defineProvider({
id: "qwen-token-plan",
models: (ctx) => qwenTokenPlanModels(ctx, "alibaba-token-plan", INTERNATIONAL_BASE_URL),
compat: { "openai-completions": QWEN_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,7 @@
import { defineProvider } from "../define.ts";
// The unauthenticated public catalog; authenticated clients overlay it at runtime.
export default defineProvider({
id: "radius",
models: (ctx) => structuredClone(ctx.sources.radius),
});

View file

@ -0,0 +1,58 @@
import type { OpenAICompletionsCompat, ThinkingLevelMap } from "../../src/types.ts";
import { defineProvider, modelsDevModels } from "../define.ts";
const BASE_COMPAT: OpenAICompletionsCompat = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
supportsStrictMode: false,
supportsLongCacheRetention: false,
};
const TOGGLE_REASONING_COMPAT: OpenAICompletionsCompat = { ...BASE_COMPAT, thinkingFormat: "together" };
const REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
...BASE_COMPAT,
supportsReasoningEffort: true,
thinkingFormat: "openai",
};
const TOGGLE_REASONING_EFFORT_COMPAT: OpenAICompletionsCompat = {
...TOGGLE_REASONING_COMPAT,
supportsReasoningEffort: true,
};
const REASONING_EFFORT_MODELS = new Set(["openai/gpt-oss-20b", "openai/gpt-oss-120b"]);
const TOGGLE_REASONING_EFFORT_MODELS = new Set(["deepseek-ai/DeepSeek-V4-Pro-0813"]);
const REASONING_EFFORT_LEVELS: ThinkingLevelMap = { off: null, minimal: null };
const DEEPSEEK_V4_LEVELS: ThinkingLevelMap = { minimal: null, low: null, medium: null, high: "high", xhigh: null };
const TOGGLE_REASONING_LEVELS: ThinkingLevelMap = { minimal: null, low: null, medium: null };
export default defineProvider({
id: "together",
models: (ctx) =>
modelsDevModels(ctx, "togetherai", {
api: "openai-completions",
baseUrl: "https://api.together.ai/v1",
skipDeprecated: true,
map: (model) => {
if (!model.reasoning) return { ...model, compat: BASE_COMPAT };
if (REASONING_EFFORT_MODELS.has(model.id)) {
return { ...model, compat: REASONING_EFFORT_COMPAT, thinkingLevelMap: { ...REASONING_EFFORT_LEVELS } };
}
if (TOGGLE_REASONING_EFFORT_MODELS.has(model.id)) {
return { ...model, compat: TOGGLE_REASONING_EFFORT_COMPAT, thinkingLevelMap: { ...DEEPSEEK_V4_LEVELS } };
}
return { ...model, compat: TOGGLE_REASONING_COMPAT, thinkingLevelMap: { ...TOGGLE_REASONING_LEVELS } };
},
}),
compat: {
"openai-completions": {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
thinkingFormat: "together",
supportsLongCacheRetention: false,
},
},
});

View file

@ -0,0 +1,32 @@
import type { ClassifierModel } from "../../src/types.ts";
import { defineProvider } from "../define.ts";
export default defineProvider({
id: "typesafe",
classifiers: (ctx): ClassifierModel<"typesafe-system-one">[] => {
const metadata = ctx.sources.modelsDevDecisions["typesafe/jev-latest"];
if (!metadata || metadata.type !== "decision") {
if (ctx.strict) throw new Error("models.dev did not return decision model typesafe/jev-latest");
return [];
}
return [
{
type: "classifier",
id: "jev-latest",
name: metadata.name,
api: "typesafe-system-one",
provider: "typesafe",
baseUrl: "https://api.typesafe.ai/v1/",
// models.dev lists text input only, but the API accepts image assets as an experimental
// feature behind an opt-in header, which the typesafe-system-one API sends.
// https://github.com/typesafe-ai/typesafe-sdk-js (v0.7.0 changelog)
input: ["text", "image"],
// The canonical models.dev entry has no direct-provider pricing. System One reports token usage,
// so classify() results carry token counts but price them at zero.
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: metadata.limit?.context || 64000,
compat: { imageInput: "assets", sendImageOptInHeader: true },
},
];
},
});

View file

@ -0,0 +1,14 @@
import { defineProvider } from "../define.ts";
import { isKimiK3FamilyId, KIMI_K3_MAX_TOKENS } from "../families/kimi.ts";
export default defineProvider({
id: "vercel-ai-gateway",
models: (ctx) =>
structuredClone(ctx.sources.aiGateway.chat).map((model) =>
// Keep Kimi K3's canonical output limit when gateway metadata is missing or incorrect.
model.id.startsWith("moonshotai/") && isKimiK3FamilyId(model.id.slice("moonshotai/".length))
? { ...model, maxTokens: KIMI_K3_MAX_TOKENS }
: model,
),
classifiers: (ctx) => structuredClone(ctx.sources.aiGateway.classifiers),
});

View file

@ -0,0 +1,29 @@
import { defineProvider, mergeThinking, modelsDevModels } from "../define.ts";
// Retired or unsuitable for pi's built-in catalog.
const EXCLUDED_MODEL_IDS = new Set([
"grok-3",
"grok-3-fast",
"grok-4.20-0309-non-reasoning",
"grok-4.20-0309-reasoning",
"grok-build-0.1",
"grok-code-fast-1",
]);
export default defineProvider({
id: "xai",
models: (ctx) =>
modelsDevModels(ctx, "xai", {
api: "openai-responses",
baseUrl: "https://api.x.ai/v1",
compat: { supportsLongCacheRetention: false },
include: (source) => !EXCLUDED_MODEL_IDS.has(source.id),
}),
finalize: (model) => {
// Models without verified effort options must not send the undocumented
// "none"/"minimal" efforts.
if (model.api === "openai-responses" && model.thinkingLevelMap === undefined) {
mergeThinking(model, { off: null, minimal: null });
}
},
});

View file

@ -0,0 +1,8 @@
import { defineProvider } from "../define.ts";
import { XIAOMI_COMPLETIONS_COMPAT, xiaomiModels } from "./xiaomi.ts";
export default defineProvider({
id: "xiaomi-token-plan-ams",
models: (ctx) => xiaomiModels(ctx, "https://token-plan-ams.xiaomimimo.com/v1"),
compat: { "openai-completions": XIAOMI_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,8 @@
import { defineProvider } from "../define.ts";
import { XIAOMI_COMPLETIONS_COMPAT, xiaomiModels } from "./xiaomi.ts";
export default defineProvider({
id: "xiaomi-token-plan-cn",
models: (ctx) => xiaomiModels(ctx, "https://token-plan-cn.xiaomimimo.com/v1"),
compat: { "openai-completions": XIAOMI_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,8 @@
import { defineProvider } from "../define.ts";
import { XIAOMI_COMPLETIONS_COMPAT, xiaomiModels } from "./xiaomi.ts";
export default defineProvider({
id: "xiaomi-token-plan-sgp",
models: (ctx) => xiaomiModels(ctx, "https://token-plan-sgp.xiaomimimo.com/v1"),
compat: { "openai-completions": XIAOMI_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,46 @@
import type { Model } from "../../src/types.ts";
import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts";
/**
* Xiaomi MiMo. The `xiaomi` provider targets the API billing endpoint; the
* `xiaomi-token-plan-*` providers cover prepaid Token Plan endpoints per region.
* MiMo Chat Completions only documents thinking.type enabled/disabled and
* rejects some reasoning_effort values (e.g. "minimal"). Xiaomi documents all
* effort levels as identical, so expose only off and high.
*/
export function xiaomiModels(ctx: ProviderContext, baseUrl: string) {
return modelsDevModels(ctx, ctx.providerId, {
api: "openai-completions",
baseUrl,
skipDeprecated: true,
recordReasoning: false,
compat: {
supportsReasoningEffort: false,
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
},
map: (model): Model<"openai-completions"> =>
model.reasoning
? {
...model,
thinkingLevelMap: {
off: "off",
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: null,
},
}
: model,
});
}
export const XIAOMI_COMPLETIONS_COMPAT = { supportsStrictMode: true } as const;
export default defineProvider({
id: "xiaomi",
models: (ctx) => xiaomiModels(ctx, "https://api.xiaomimimo.com/v1"),
compat: { "openai-completions": XIAOMI_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,8 @@
import { defineProvider } from "../define.ts";
import { ZAI_COMPLETIONS_COMPAT, zaiCodingPlanModels } from "./zai.ts";
export default defineProvider({
id: "zai-coding-cn",
models: (ctx) => zaiCodingPlanModels(ctx, "zhipuai-coding-plan", "https://open.bigmodel.cn/api/coding/paas/v4"),
compat: { "openai-completions": ZAI_COMPLETIONS_COMPAT },
});

View file

@ -0,0 +1,50 @@
import type { Model } from "../../src/types.ts";
import { defineProvider, modelsDevModels, type ProviderContext } from "../define.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
import { modelsDevChatModels, modelsDevCost } from "../sources/models-dev.ts";
// Tool-call streaming (`tool_stream`) is rejected by the GLM-4.5 generation.
const TOOL_STREAM_UNSUPPORTED_MODEL_IDS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
/** The Z.AI coding plans. The international and China plans list the same models. */
export function zaiCodingPlanModels(ctx: ProviderContext, source: string, baseUrl: string) {
// Coding-plan entries carry plan prices; show the pay-as-you-go API rates.
const apiModels = new Map(modelsDevChatModels(ctx.sources.modelsDev, "zai").map((model) => [model.id, model]));
return modelsDevModels(ctx, source, {
api: "openai-completions",
baseUrl,
map: (model, source): Model<"openai-completions"> => {
const thinkingLevelMap = getEffortThinkingLevelMap(source.reasoningOptions ?? []);
if (thinkingLevelMap && (model.id === "glm-5.2" || model.id === "glm-5.2-highspeed")) {
thinkingLevelMap.off = "none";
}
const apiModel = apiModels.get(model.id);
return {
...model,
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
cost: modelsDevCost(apiModel?.source.cost ? apiModel.cost : source.cost),
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
...(thinkingLevelMap ? { supportsReasoningEffort: true } : {}),
...(!TOOL_STREAM_UNSUPPORTED_MODEL_IDS.has(model.id) ? { zaiToolStream: true } : {}),
},
};
},
});
}
export const ZAI_COMPLETIONS_COMPAT = {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
thinkingFormat: "zai",
supportsStrictMode: true,
} as const;
export default defineProvider({
id: "zai",
models: (ctx) => zaiCodingPlanModels(ctx, "zai-coding-plan", "https://api.z.ai/api/coding/paas/v4"),
compat: { "openai-completions": ZAI_COMPLETIONS_COMPAT },
});

View file

@ -1,12 +1,5 @@
import type { ThinkingLevel, ThinkingLevelMap } from "../src/types.ts";
export type ModelsDevReasoningOption =
| { type: "toggle" }
| {
type: "effort";
values: Array<"none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "default" | null>;
}
| { type: "budget_tokens"; min?: number; max?: number };
import type { ModelsDevReasoningOption } from "./sources/models-dev.ts";
const THINKING_LEVELS: readonly ThinkingLevel[] = ["minimal", "low", "medium", "high", "xhigh", "max"];

View file

@ -0,0 +1,95 @@
import {
DEFAULT_RADIUS_GATEWAY,
getRadiusModelsFromConfig,
loadRadiusGatewayConfig,
} from "../../src/providers/radius-config.ts";
import type { Model } from "../../src/types.ts";
import {
MODELS_DEV_DECISIONS_URL,
MODELS_DEV_URL,
type ModelsDevDecisionMetadata,
type ModelsDevV2Catalog,
} from "./models-dev.ts";
import { fetchOpenRouterCatalog, type OpenRouterCatalog } from "./openrouter.ts";
import { type AiGatewayCatalog, fetchAiGatewayCatalog } from "./vercel-ai-gateway.ts";
export const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
/** Everything fetched from upstream before the catalog is built. */
export interface CatalogSources {
modelsDev: ModelsDevV2Catalog;
modelsDevDecisions: Record<string, ModelsDevDecisionMetadata>;
openRouter: OpenRouterCatalog;
aiGateway: AiGatewayCatalog;
/** Live NVIDIA NIM ids, keyed by both the raw and the normalized id. */
nvidiaModelIds: Map<string, string>;
radius: Model<"pi-messages">[];
}
async function fetchJson<T>(url: string, label: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`${label} returned ${response.status}`);
return (await response.json()) as T;
}
export function normalizeNvidiaModelId(modelId: string): string {
return modelId.toLowerCase().replaceAll("_", ".");
}
async function fetchNvidiaModelIds(): Promise<Map<string, string>> {
const data = await fetchJson<{ data?: { id: string }[] }>(`${NVIDIA_BASE_URL}/models`, "NVIDIA NIM API");
const modelIds = new Map<string, string>();
for (const model of data.data ?? []) {
modelIds.set(model.id, model.id);
modelIds.set(normalizeNvidiaModelId(model.id), model.id);
}
return modelIds;
}
async function fetchRadiusModels(): Promise<Model<"pi-messages">[]> {
const config = await loadRadiusGatewayConfig(DEFAULT_RADIUS_GATEWAY);
const models = getRadiusModelsFromConfig("radius", config);
if (models.length === 0) throw new Error("Radius API returned no models");
return models;
}
/**
* Fetch every upstream source. In strict mode any failure aborts generation;
* otherwise a failed source contributes nothing.
*/
export async function loadCatalogSources(strict: boolean): Promise<CatalogSources> {
async function load<T>(label: string, fetcher: () => Promise<T>, fallback: T): Promise<T> {
try {
console.log(`Fetching ${label}...`);
return await fetcher();
} catch (error) {
console.error(`Failed to fetch ${label}:`, error);
if (strict) throw error;
return fallback;
}
}
const modelsDev = await load(
"models.dev catalog",
() => fetchJson<ModelsDevV2Catalog>(MODELS_DEV_URL, "models.dev API"),
{},
);
const modelsDevDecisions = await load(
"models.dev decision models",
() => fetchJson<Record<string, ModelsDevDecisionMetadata>>(MODELS_DEV_DECISIONS_URL, "models.dev decision API"),
{},
);
const nvidiaModelIds = modelsDev.nvidia?.models
? await load("NVIDIA NIM models", fetchNvidiaModelIds, new Map<string, string>())
: new Map<string, string>();
const openRouter = await load("OpenRouter models", fetchOpenRouterCatalog, {
chat: [],
images: [],
classifiers: [],
});
if (strict && openRouter.images.length === 0) throw new Error("OpenRouter API returned no usable image models");
const aiGateway = await load("Vercel AI Gateway models", fetchAiGatewayCatalog, { chat: [], classifiers: [] });
const radius = await load("Radius models", fetchRadiusModels, []);
return { modelsDev, modelsDevDecisions, openRouter, aiGateway, nvidiaModelIds, radius };
}

View file

@ -0,0 +1,242 @@
import type { ModelCost } from "../../src/types.ts";
// models.dev v2 catalog. The schema is published as experimental, so everything
// the catalog reads goes through `normalizeModelsDevModel()` below.
// https://github.com/anomalyco/models.dev/blob/dev/packages/core/src/schema-v2.ts
export const MODELS_DEV_URL = "https://models.dev/experimental/v2.0/api.json";
// v2 has no provider-agnostic decision metadata endpoint; v1 still serves it.
export const MODELS_DEV_DECISIONS_URL = "https://models.dev/models.json?type=decision";
export type ModelsDevReasoningOption =
| { type: "toggle" }
| {
type: "effort";
values: Array<"none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "default" | null>;
}
| { type: "budget_tokens"; min?: number; max?: number };
type ModelsDevEffort = Extract<ModelsDevReasoningOption, { type: "effort" }>["values"][number];
interface V2CachePrices {
read?: number;
write?: number | Record<string, number>;
explicit?: { read?: number; write?: number | Record<string, number> };
}
interface V2CostOverride {
when: Record<string, unknown>;
price?: number;
cache?: V2CachePrices;
}
interface V2ModalityCost {
input?: { unit?: string; price: number; cache?: V2CachePrices; overrides?: V2CostOverride[] };
output?: { unit?: string; price: number; overrides?: V2CostOverride[] };
}
interface V2ApiEntry {
base_url: string;
path?: string;
sdk?: { aisdk?: string };
compatibility?: Record<string, unknown>;
capabilities?: Record<string, unknown>;
}
/** The subset of a models.dev v2 model the catalog reads. */
export interface ModelsDevV2Model {
id: string;
type: string;
name: string;
status?: string;
modalities: { input: string[]; output: string[] };
capabilities: {
tools: { supported: boolean };
reasoning: {
supported: boolean;
toggle?: boolean;
effort?: ModelsDevEffort[];
budget?: { min?: number; max?: number };
};
structured_output?: boolean;
};
limit?: { context?: number; input?: number; output?: number };
cost?: { text?: V2ModalityCost };
api: Record<string, V2ApiEntry>;
}
export type ModelsDevV2Catalog = Record<string, { models?: Record<string, ModelsDevV2Model> }>;
/** Prices as models.dev lists them. Missing prices stay undefined. */
export interface ModelsDevCost {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
/** Context-size tiers. Unlisted rates fall back to the base rates. */
tiers?: Array<{ above: number; input?: number; output?: number; cacheRead?: number; cacheWrite?: number }>;
}
/** One API a model is served on, in the order models.dev lists them. */
export interface ModelsDevApi {
protocol: string;
baseUrl: string;
aisdk?: string;
}
/** A models.dev model normalized for the catalog. */
export interface ModelsDevModel {
id: string;
name: string;
type: string;
status?: string;
toolCall: boolean;
structuredOutput: boolean;
reasoning: boolean;
/** Undefined when the model does not reason. */
reasoningOptions?: ModelsDevReasoningOption[];
imageInput: boolean;
textInput: boolean;
textOutput: boolean;
contextWindow?: number;
maxTokens?: number;
cost: ModelsDevCost;
apis: ModelsDevApi[];
/** The full v2 entry, for fields not normalized above. */
source: ModelsDevV2Model;
}
/** Provider-agnostic decision model metadata from models.dev. */
export interface ModelsDevDecisionMetadata {
id: string;
type?: string;
name: string;
limit?: { context?: number };
}
function firstPrice(write: number | Record<string, number> | undefined): number | undefined {
return typeof write === "object" ? Object.values(write)[0] : write;
}
// models.dev lists one cache write price per TTL; the first is the host's default.
function cacheWritePrice(cache: V2CachePrices | undefined): number | undefined {
return firstPrice(cache?.write) ?? firstPrice(cache?.explicit?.write);
}
function contextThreshold(when: Record<string, unknown>): number | undefined {
const keys = Object.keys(when);
if (keys.length !== 1 || keys[0] !== "context") return undefined;
const context = when.context as { gt?: unknown; gte?: unknown };
const size = context.gt ?? context.gte;
return typeof size === "number" ? size : undefined;
}
function normalizeCost(cost: ModelsDevV2Model["cost"]): ModelsDevCost {
const input = cost?.text?.input;
const output = cost?.text?.output;
const perToken = (unit: string | undefined) => unit === undefined || unit === "token";
if (!input || !output || !perToken(input.unit) || !perToken(output.unit)) return {};
// Only context-size overrides map onto pi's cost tiers. Region, service-tier,
// and time-based prices have no pi equivalent yet.
const tiers = new Map<number, NonNullable<ModelsDevCost["tiers"]>[number]>();
const tier = (above: number) => {
let entry = tiers.get(above);
if (!entry) {
entry = { above };
tiers.set(above, entry);
}
return entry;
};
for (const override of input.overrides ?? []) {
const above = contextThreshold(override.when);
if (above === undefined) continue;
const entry = tier(above);
if (override.price !== undefined) entry.input = override.price;
if (override.cache?.read !== undefined) entry.cacheRead = override.cache.read;
const cacheWrite = cacheWritePrice(override.cache);
if (cacheWrite !== undefined) entry.cacheWrite = cacheWrite;
}
for (const override of output.overrides ?? []) {
const above = contextThreshold(override.when);
if (above === undefined) continue;
if (override.price !== undefined) tier(above).output = override.price;
}
return {
input: input.price,
output: output.price,
cacheRead: input.cache?.read,
cacheWrite: cacheWritePrice(input.cache),
...(tiers.size > 0 ? { tiers: [...tiers.values()] } : {}),
};
}
function reasoningOptions(reasoning: ModelsDevV2Model["capabilities"]["reasoning"]): ModelsDevReasoningOption[] {
const options: ModelsDevReasoningOption[] = [];
if (reasoning.toggle) options.push({ type: "toggle" });
if (reasoning.effort !== undefined) options.push({ type: "effort", values: reasoning.effort });
if (reasoning.budget !== undefined) options.push({ type: "budget_tokens", ...reasoning.budget });
return options;
}
export function normalizeModelsDevModel(model: ModelsDevV2Model): ModelsDevModel {
const input = model.modalities?.input ?? [];
const reasoning = model.capabilities?.reasoning;
return {
id: model.id,
name: model.name,
type: model.type,
status: model.status,
toolCall: model.capabilities?.tools?.supported === true,
structuredOutput: model.capabilities?.structured_output === true,
reasoning: reasoning?.supported === true,
reasoningOptions: reasoning?.supported ? reasoningOptions(reasoning) : undefined,
imageInput: input.some((modality) => modality === "image" || modality.startsWith("image/")),
textInput: input.includes("text"),
textOutput: (model.modalities?.output ?? []).includes("text"),
contextWindow: model.limit?.context,
maxTokens: model.limit?.output,
cost: normalizeCost(model.cost),
apis: Object.entries(model.api ?? {}).map(([protocol, entry]) => ({
protocol,
baseUrl: entry.base_url,
aisdk: entry.sdk?.aisdk,
})),
source: model,
};
}
export function roundCost(value: number): number {
return Number(value.toFixed(6));
}
/** Converts models.dev prices to pi's cost shape. Missing rates are free. */
export function modelsDevCost(cost: ModelsDevCost): ModelCost {
const base = {
input: cost.input || 0,
output: cost.output || 0,
cacheRead: cost.cacheRead || 0,
cacheWrite: cost.cacheWrite || 0,
};
const tiers = cost.tiers?.map((tier) => ({
inputTokensAbove: tier.above,
input: tier.input ?? base.input,
output: tier.output ?? base.output,
cacheRead: tier.cacheRead ?? base.cacheRead,
cacheWrite: tier.cacheWrite ?? base.cacheWrite,
}));
return tiers && tiers.length > 0 ? { ...base, tiers } : base;
}
/**
* Chat models of one models.dev provider. Specialized types (image, speech,
* realtime, decision, ...) only appear in v2 and are left out here.
*/
export function modelsDevChatModels(catalog: ModelsDevV2Catalog, provider: string): ModelsDevModel[] {
return (
Object.values(catalog[provider]?.models ?? {})
// Unlike pi models, models.dev v2 always states the type.
.filter(({ type }) => type === "chat")
.map(normalizeModelsDevModel)
);
}

View file

@ -1,6 +1,29 @@
import { type OpenRouterModel, openRouterCost } from "../src/providers/openrouter-api.ts";
import type { ClassifierModel, ImageModel, Model } from "../src/types.ts";
import { getOpenRouterThinkingLevelMap, type OpenRouterReasoningMetadata } from "./openrouter-reasoning-options.ts";
import { type OpenRouterModel, openRouterCost } from "../../src/providers/openrouter-api.ts";
import type { ClassifierModel, ImageModel, Model, ThinkingLevel, ThinkingLevelMap } from "../../src/types.ts";
import { getEffortThinkingLevelMap } from "../reasoning.ts";
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
export interface OpenRouterReasoningMetadata {
mandatory?: boolean;
default_enabled?: boolean;
supported_efforts?: Array<ThinkingLevel | "none">;
default_effort?: ThinkingLevel | "none";
}
/** Convert OpenRouter's reasoning metadata into Pi model capabilities. */
export function getOpenRouterThinkingLevelMap(
reasoning: OpenRouterReasoningMetadata | undefined,
): ThinkingLevelMap | undefined {
if (!reasoning) return undefined;
if (!reasoning.supported_efforts?.length) return reasoning.mandatory === true ? { off: null } : undefined;
// OpenRouter's supported_efforts uses the same effort values as models.dev reasoning_options,
// so both sources can share the same Pi thinking-level conversion.
const map = getEffortThinkingLevelMap([{ type: "effort", values: reasoning.supported_efforts }]);
if (!map) return reasoning.mandatory === true ? { off: null } : undefined;
return { ...map, off: reasoning.mandatory === true ? null : "none" };
}
export interface OpenRouterModelListItem extends OpenRouterModel {
name: string;
@ -106,3 +129,20 @@ export function buildOpenRouterCatalog(
return { chat, images, classifiers };
}
async function fetchOpenRouterList(query: string): Promise<OpenRouterModelListItem[]> {
const response = await fetch(`${OPENROUTER_MODELS_URL}${query}`);
if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`);
const data = (await response.json()) as { data?: OpenRouterModelListItem[] };
return data.data ?? [];
}
/** Fetch the tool-capable chat, image, and decision listings. */
export async function fetchOpenRouterCatalog(): Promise<OpenRouterCatalog> {
const [listed, imageListed, decisionListed] = await Promise.all([
fetchOpenRouterList(""),
fetchOpenRouterList("?output_modalities=image"),
fetchOpenRouterList("?output_modalities=decisions"),
]);
return buildOpenRouterCatalog(listed, imageListed, decisionListed);
}

View file

@ -0,0 +1,140 @@
import type { ClassifierModel, Model, ModelCost, ModelCostRates, ModelCostTier } from "../../src/types.ts";
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1/models";
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
// TypeSafe-compatible System One endpoint for evaluation models.
// https://vercel.com/docs/ai-gateway/sdks-and-apis/typesafe
const AI_GATEWAY_TYPESAFE_BASE_URL = "https://ai-gateway.vercel.sh/typesafe/v1";
/** A prompt-length bracket. `min` is inclusive and `max` exclusive, both in prompt tokens. */
export interface AiGatewayPriceTier {
cost?: string | number;
min?: number;
max?: number;
}
/** Vercel AI Gateway pricing in $/token. */
export interface AiGatewayPricing {
input?: string | number;
output?: string | number;
input_cache_read?: string | number;
input_cache_write?: string | number;
input_tiers?: AiGatewayPriceTier[];
output_tiers?: AiGatewayPriceTier[];
input_cache_read_tiers?: AiGatewayPriceTier[];
input_cache_write_tiers?: AiGatewayPriceTier[];
}
const RATE_FIELDS = [
{ rate: "input", tiers: "input_tiers" },
{ rate: "output", tiers: "output_tiers" },
{ rate: "cacheRead", tiers: "input_cache_read_tiers" },
{ rate: "cacheWrite", tiers: "input_cache_write_tiers" },
] as const;
function perMillion(value: string | number | undefined): number {
const parsed = typeof value === "number" ? value : parseFloat(value ?? "0");
return Number.isFinite(parsed) ? Number((parsed * 1_000_000).toFixed(6)) : 0;
}
/**
* Convert AI Gateway pricing to $/million tokens. Each rate lists its own prompt-length brackets;
* every bracket start above zero becomes a request-wide tier with the rates in effect there.
*/
export function getAiGatewayCost(pricing: AiGatewayPricing | undefined): ModelCost {
const base: ModelCostRates = {
input: perMillion(pricing?.input),
output: perMillion(pricing?.output),
cacheRead: perMillion(pricing?.input_cache_read),
cacheWrite: perMillion(pricing?.input_cache_write),
};
const starts = new Set<number>();
for (const field of RATE_FIELDS) {
for (const bracket of pricing?.[field.tiers] ?? []) {
if (bracket.min !== undefined && bracket.min > 0) starts.add(bracket.min);
}
}
const tiers = [...starts]
.sort((a, b) => a - b)
.map((start): ModelCostTier => {
const tier: ModelCostTier = { inputTokensAbove: start - 1, ...base };
for (const field of RATE_FIELDS) {
const bracket = pricing?.[field.tiers]?.find(
(candidate) => (candidate.min ?? 0) <= start && (candidate.max === undefined || start < candidate.max),
);
if (bracket?.cost !== undefined) tier[field.rate] = perMillion(bracket.cost);
}
return tier;
});
return tiers.length > 0 ? { ...base, tiers } : base;
}
interface AiGatewayModel {
id: string;
name?: string;
type?: string;
context_window?: number;
max_tokens?: number;
tags?: string[];
pricing?: AiGatewayPricing;
}
export interface AiGatewayCatalog {
chat: Model<"anthropic-messages">[];
classifiers: ClassifierModel<"typesafe-system-one">[];
}
/** Fetch tool-capable chat models and evaluation (classifier) models. */
export async function fetchAiGatewayCatalog(): Promise<AiGatewayCatalog> {
const response = await fetch(AI_GATEWAY_MODELS_URL);
if (!response.ok) throw new Error(`Vercel AI Gateway API returned ${response.status}`);
const data = (await response.json()) as { data?: unknown };
const items = Array.isArray(data.data) ? (data.data as AiGatewayModel[]) : [];
const chat: AiGatewayCatalog["chat"] = [];
const classifiers: AiGatewayCatalog["classifiers"] = [];
for (const model of items) {
// Evaluation models such as TypeSafe's Jev are served through the
// TypeSafe-compatible System One endpoint.
if (model.type === "evaluation") {
classifiers.push({
type: "classifier",
id: model.id,
name: model.name || model.id,
api: "typesafe-system-one",
provider: "vercel-ai-gateway",
baseUrl: AI_GATEWAY_TYPESAFE_BASE_URL,
input: ["text"],
cost: {
input: perMillion(model.pricing?.input),
output: perMillion(model.pricing?.output),
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: model.context_window || 4096,
});
continue;
}
const tags = Array.isArray(model.tags) ? model.tags : [];
// Only include models that support tools
if (!tags.includes("tool-use")) continue;
chat.push({
id: model.id,
name: model.name || model.id,
api: "anthropic-messages",
baseUrl: AI_GATEWAY_BASE_URL,
provider: "vercel-ai-gateway",
reasoning: tags.includes("reasoning"),
input: tags.includes("vision") ? ["text", "image"] : ["text"],
compat: { allowEmptySignature: true },
cost: getAiGatewayCost(model.pricing),
contextWindow: model.context_window || 4096,
maxTokens: model.max_tokens || 4096,
});
}
return { chat, classifiers };
}

View file

@ -57,9 +57,9 @@
],
"scripts": {
"clean": "shx rm -rf dist",
"generate-models": "node scripts/generate-models.ts --strict",
"hydrate-model-data": "node scripts/generate-models.ts --strict --data-only",
"generate-model-catalog": "node scripts/generate-models.ts --strict --json-only --json-output ../../.artifacts/model-catalog",
"generate-models": "node catalog/generate.ts --strict",
"hydrate-model-data": "node catalog/generate.ts --strict --data-only",
"generate-model-catalog": "node catalog/generate.ts --strict --json-only --json-output ../../.artifacts/model-catalog",
"check:model-data": "node scripts/check-model-data.ts",
"build": "npm run generate-models && npm run build:offline",
"build:offline": "npm run check:model-data && tsc -p tsconfig.build.json && shx rm -rf dist/providers/data && shx cp -r src/providers/data dist/providers/data",

View file

@ -1,67 +0,0 @@
import type { ModelCost, ModelCostRates, ModelCostTier } from "../src/types.ts";
/** A prompt-length bracket. `min` is inclusive and `max` exclusive, both in prompt tokens. */
export interface AiGatewayPriceTier {
cost?: string | number;
min?: number;
max?: number;
}
/** Vercel AI Gateway pricing in $/token. */
export interface AiGatewayPricing {
input?: string | number;
output?: string | number;
input_cache_read?: string | number;
input_cache_write?: string | number;
input_tiers?: AiGatewayPriceTier[];
output_tiers?: AiGatewayPriceTier[];
input_cache_read_tiers?: AiGatewayPriceTier[];
input_cache_write_tiers?: AiGatewayPriceTier[];
}
const RATE_FIELDS = [
{ rate: "input", tiers: "input_tiers" },
{ rate: "output", tiers: "output_tiers" },
{ rate: "cacheRead", tiers: "input_cache_read_tiers" },
{ rate: "cacheWrite", tiers: "input_cache_write_tiers" },
] as const;
function perMillion(value: string | number | undefined): number {
const parsed = typeof value === "number" ? value : parseFloat(value ?? "0");
return Number.isFinite(parsed) ? Number((parsed * 1_000_000).toFixed(6)) : 0;
}
/**
* Convert AI Gateway pricing to $/million tokens. Each rate lists its own prompt-length brackets;
* every bracket start above zero becomes a request-wide tier with the rates in effect there.
*/
export function getAiGatewayCost(pricing: AiGatewayPricing | undefined): ModelCost {
const base: ModelCostRates = {
input: perMillion(pricing?.input),
output: perMillion(pricing?.output),
cacheRead: perMillion(pricing?.input_cache_read),
cacheWrite: perMillion(pricing?.input_cache_write),
};
const starts = new Set<number>();
for (const field of RATE_FIELDS) {
for (const bracket of pricing?.[field.tiers] ?? []) {
if (bracket.min !== undefined && bracket.min > 0) starts.add(bracket.min);
}
}
const tiers = [...starts]
.sort((a, b) => a - b)
.map((start): ModelCostTier => {
const tier: ModelCostTier = { inputTokensAbove: start - 1, ...base };
for (const field of RATE_FIELDS) {
const bracket = pricing?.[field.tiers]?.find(
(candidate) => (candidate.min ?? 0) <= start && (candidate.max === undefined || start < candidate.max),
);
if (bracket?.cost !== undefined) tier[field.rate] = perMillion(bracket.cost);
}
return tier;
});
return tiers.length > 0 ? { ...base, tiers } : base;
}

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
import type { ThinkingLevel, ThinkingLevelMap } from "../src/types.ts";
import { getEffortThinkingLevelMap } from "./models-dev-reasoning-options.ts";
export interface OpenRouterReasoningMetadata {
mandatory?: boolean;
default_enabled?: boolean;
supported_efforts?: Array<ThinkingLevel | "none">;
default_effort?: ThinkingLevel | "none";
}
/** Convert OpenRouter's reasoning metadata into Pi model capabilities. */
export function getOpenRouterThinkingLevelMap(
reasoning: OpenRouterReasoningMetadata | undefined,
): ThinkingLevelMap | undefined {
if (!reasoning) return undefined;
if (!reasoning.supported_efforts?.length) return reasoning.mandatory === true ? { off: null } : undefined;
// OpenRouter's supported_efforts uses the same effort values as models.dev reasoning_options,
// so both sources can share the same Pi thinking-level conversion.
const map = getEffortThinkingLevelMap([{ type: "effort", values: reasoning.supported_efforts }]);
if (!map) return reasoning.mandatory === true ? { off: null } : undefined;
return { ...map, off: reasoning.mandatory === true ? null : "none" };
}

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import { AMAZON_BEDROCK_CLASSIFIER_MODELS, AMAZON_BEDROCK_IMAGE_MODELS, AMAZON_BEDROCK_MODELS } from "./providers/amazon-bedrock.models.ts";

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/amazon-bedrock.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/ant-ling.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/anthropic.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/azure.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/baseten.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cerebras.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cloudflare-ai-gateway.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/cloudflare-workers-ai.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/deepseek.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/fireworks.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/github-copilot.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/google-vertex.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/google.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/groq.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/huggingface.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/kimi-coding.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/meta.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/minimax-cn.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/minimax.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/mistral.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/moonshotai-cn.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/moonshotai.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/nvidia.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openai-codex.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openai.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/opencode-go.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/opencode.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/openrouter.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/qwen-token-plan-cn.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/qwen-token-plan-individual.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/qwen-token-plan.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/radius.json" with { type: "json" };

View file

@ -1,4 +1,4 @@
// This file is auto-generated by scripts/generate-models.ts
// This file is auto-generated by catalog/generate.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/together.json" with { type: "json" };

Some files were not shown because too many files have changed in this diff Show more