Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] e35806b331 fix: [missing-model] pioneer: nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16 2026-08-19 08:38:14 +00:00
920 changed files with 1913 additions and 10220 deletions
@@ -1,19 +0,0 @@
name = "DeepSeek V4 Flash Vision Exp"
description = "Experimental multimodal DeepSeek V4 Flash model for image understanding, coding, and agentic work"
family = "deepseek-flash"
release_date = "2026-08-21"
last_updated = "2026-08-21"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[limit]
context = 1_000_000
output = 384_000
[modalities]
input = ["text", "image"]
output = ["text"]
-22
View File
@@ -1,22 +0,0 @@
# https://ofox.ai/models/deepseek/deepseek-v4-pro-0423
# https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro
name = "DeepSeek V4 Pro 0423"
description = "DeepSeek V4 Pro initial snapshot with million-token context and support for thinking and non-thinking modes"
family = "deepseek-thinking"
release_date = "2026-04-23"
last_updated = "2026-04-23"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-05"
open_weights = true
[limit]
context = 1_000_000
output = 384_000
[modalities]
input = ["text"]
output = ["text"]
+2 -8
View File
@@ -1,16 +1,14 @@
# https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813
name = "DeepSeek V4 Pro 0813"
description = "DeepSeek V4 Pro snapshot with million-token context and support for thinking and non-thinking modes"
family = "deepseek-thinking"
release_date = "2026-08-12"
last_updated = "2026-08-22"
last_updated = "2026-08-12"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
license = "MIT"
open_weights = false
[limit]
context = 1_000_000
@@ -19,7 +17,3 @@ output = 384_000
[modalities]
input = ["text"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813"
+5 -5
View File
@@ -1,15 +1,15 @@
# Tracks the current Gemini Flash release (gemini-3.7-flash).
# Tracks the current Gemini Flash release (gemini-3.5-flash).
name = "Gemini Flash Latest"
description = "High-efficiency Gemini model for agentic workflows, coding, and multimodal reasoning"
description = "Fast Gemini model balancing multimodal reasoning, tool use, and cost"
family = "gemini-flash"
release_date = "2026-08-13"
last_updated = "2026-08-13"
release_date = "2026-05-19"
last_updated = "2026-05-19"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2026-03"
knowledge = "2025-01"
open_weights = false
[limit]
+5 -5
View File
@@ -1,15 +1,15 @@
# Tracks the current Gemini Flash-Lite release (gemini-3.5-flash-lite).
# Tracks the current Gemini Flash-Lite release (gemini-3.1-flash-lite).
name = "Gemini Flash-Lite Latest"
description = "Fast Gemini model balancing multimodal reasoning, tool use, and cost"
description = "Low-latency Gemini model for high-volume multimodal and agent workloads"
family = "gemini-flash-lite"
release_date = "2026-07-21"
last_updated = "2026-07-21"
release_date = "2026-05-07"
last_updated = "2026-05-07"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2026-03"
knowledge = "2025-01"
open_weights = false
[limit]
-25
View File
@@ -1,25 +0,0 @@
# Sources (accessed 2026-08-19):
# - https://z.ai/blog/glm-4.6v
# - https://huggingface.co/zai-org/GLM-4.6V-Flash
name = "GLM-4.6V-Flash"
description = "Lightweight GLM vision model for visual reasoning, documents, and multimodal agents"
family = "glm"
release_date = "2025-12-08"
last_updated = "2025-12-08"
attachment = true
reasoning = true
temperature = true
tool_call = true
open_weights = true
[limit]
context = 128_000
output = 32_768
[modalities]
input = ["text", "image", "video"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/zai-org/GLM-4.6V-Flash"
-1
View File
@@ -28,7 +28,6 @@
"huggingface:sync": "bun ./packages/core/script/sync-models.ts huggingface",
"kilo:sync": "bun ./packages/core/script/sync-models.ts kilo",
"llmgateway:sync": "bun ./packages/core/script/sync-models.ts llmgateway",
"llmgateway-providers:sync": "bun ./packages/core/script/sync-models.ts llmgateway-providers",
"requesty:sync": "bun ./packages/core/script/sync-models.ts requesty",
"merge-gateway:sync": "bun ./packages/core/script/sync-models.ts merge-gateway",
"nano-gpt:sync": "bun ./packages/core/script/sync-models.ts nano-gpt",
-1
View File
@@ -52,7 +52,6 @@ export const ModelFamilyValues = [
// Meta Muse
"muse",
"muse-free",
// Alibaba Qwen
"qwen",
-2
View File
@@ -5,13 +5,11 @@ export const MAX_CREATED_MODELS = 10;
export const MAX_DELETED_MODELS = 10;
export const MAX_MODEL_CHURN = 15;
const REVIEWED_REASONING_PROVIDERS = new Set([
"crossmodel",
"edenai",
"empiriolabs",
"hyper",
"kilo",
"llmgateway",
"llmgateway-providers",
"merge-gateway",
"nano-gpt",
"openrouter",
+4 -25
View File
@@ -21,7 +21,7 @@ import { hyper } from "./providers/hyper.js";
import { huggingface } from "./providers/huggingface.js";
import { inceptron } from "./providers/inceptron.js";
import { kilo } from "./providers/kilo.js";
import { llmgateway, llmgatewayProviders } from "./providers/llmgateway.js";
import { llmgateway } from "./providers/llmgateway.js";
import { mergeGateway } from "./providers/merge-gateway.js";
import { nanoGpt } from "./providers/nano-gpt.js";
import { openai } from "./providers/openai.js";
@@ -98,17 +98,7 @@ export interface SyncProvider<SourceModel> {
existing(id: string): ExistingModel | undefined;
authored(id: string): ExistingModel | undefined;
},
): {
id: string;
model: SyncedModel;
metadata?: { id: string; model: SyncedMetadata };
/**
* Leading comment block for the written file when it has none of its own
* (e.g. the wire-path header every toggle reasoning control requires). A
* header already present on the existing file always wins.
*/
header?: string;
} | undefined;
): { id: string; model: SyncedModel; metadata?: { id: string; model: SyncedMetadata } } | undefined;
}
export interface SyncResult {
@@ -141,7 +131,6 @@ export const providers: {
inceptron: SyncProvider<any>;
kilo: SyncProvider<any>;
llmgateway: SyncProvider<any>;
"llmgateway-providers": SyncProvider<any>;
"merge-gateway": SyncProvider<any>;
"nano-gpt": SyncProvider<any>;
ofox: SyncProvider<any>;
@@ -173,7 +162,6 @@ export const providers: {
inceptron,
kilo,
llmgateway,
"llmgateway-providers": llmgatewayProviders,
"merge-gateway": mergeGateway,
"nano-gpt": nanoGpt,
ofox,
@@ -198,7 +186,6 @@ export const groups = {
"inceptron",
"kilo",
"llmgateway",
"llmgateway-providers",
"merge-gateway",
"nano-gpt",
"ofox",
@@ -283,16 +270,13 @@ export async function syncProvider<SourceModel>(
: preserveBaseModel(translated.model, existing.get(relativePath)?.authored);
const translatedBase = "base_model" in translatedModel ? translatedModel.base_model : undefined;
let resolvedReasoning: boolean | undefined;
let baseReasoningOptions: unknown;
if (translatedBase !== undefined) {
if (translated.metadata?.id === translatedBase) {
resolvedReasoning = translated.metadata.model.reasoning;
baseReasoningOptions = translated.metadata.model.reasoning_options;
} else {
modelMetadata ??= await readModelMetadata(provider.modelsDir);
const canonicalReasoning = modelMetadata[translatedBase]?.reasoning;
resolvedReasoning = typeof canonicalReasoning === "boolean" ? canonicalReasoning : undefined;
baseReasoningOptions = modelMetadata[translatedBase]?.reasoning_options;
}
} else {
resolvedReasoning = existing.get(relativePath)?.toml.reasoning;
@@ -301,7 +285,6 @@ export async function syncProvider<SourceModel>(
translatedModel,
existing.get(relativePath)?.authored,
resolvedReasoning,
baseReasoningOptions,
);
const withDescription = provider.preserveDescriptions === false
? withReasoningOptions
@@ -317,7 +300,7 @@ export async function syncProvider<SourceModel>(
desired.set(relativePath, {
model: parsed.data,
content: ((existing.get(relativePath)?.header || translated.header) ?? "") + formatToml(parsed.data),
content: (existing.get(relativePath)?.header ?? "") + formatToml(parsed.data),
});
}
@@ -493,7 +476,6 @@ export function preserveReasoningOptions(
model: SyncedModel,
existing: ExistingModel | undefined,
resolvedReasoning: boolean | undefined = existing?.reasoning,
baseReasoningOptions: unknown = undefined,
): SyncedModel {
if ((model.reasoning ?? resolvedReasoning) === false) {
const { reasoning_options: _reasoningOptions, ...withoutReasoningOptions } = model;
@@ -501,10 +483,7 @@ export function preserveReasoningOptions(
}
if (model.reasoning_options !== undefined) return model;
if (existing?.reasoning_options === undefined) {
// When the base model already declares reasoning_options, leave the field
// unset so the factored file inherits them — stamping [] here would
// shadow the base's real controls with "no controls".
return (model.reasoning ?? resolvedReasoning) === true && baseReasoningOptions === undefined
return (model.reasoning ?? resolvedReasoning) === true
? { ...model, reasoning_options: [] }
: model;
}
+2 -12
View File
@@ -13,16 +13,6 @@ const CANONICAL_BASE_MODEL_EXCEPTIONS = {
const EUR_TO_USD = 1.114;
const CortecsModality = z.enum(["text", "audio", "image", "video", "pdf"]);
type CortecsModality = z.infer<typeof CortecsModality>;
function modalities(values: string[]): CortecsModality[] {
const allowed = new Set<CortecsModality>(CortecsModality.options);
const result = values
.map((value) => value.toLowerCase())
.map((value) => value === "file" ? "pdf" : value)
.filter((value): value is CortecsModality => allowed.has(value as CortecsModality));
return [...new Set<CortecsModality>(result.length > 0 ? result : ["text"])];
}
export const CortecsModel = z.object({
id: z.string().min(1),
@@ -36,8 +26,8 @@ export const CortecsModel = z.object({
cache_write_cost: z.number().nonnegative().optional(),
}).passthrough(),
context_size: z.number().int().positive(),
input_modalities: z.array(z.string()).transform(modalities).default(["text"]),
output_modalities: z.array(z.string()).transform(modalities).default(["text"]),
input_modalities: z.array(CortecsModality).default(["text"]),
output_modalities: z.array(CortecsModality).default(["text"]),
supported_features: z.array(z.string()).default([]),
}).passthrough();
+12 -4
View File
@@ -22,13 +22,11 @@ function baseModelExists(modelID: string): boolean {
// CROSSMODEL_MODELS_URL overrides the endpoint (e.g. a local backend) for testing.
const API_ENDPOINT = process.env.CROSSMODEL_MODELS_URL ?? "https://www.crossmodel.ai/api/models";
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] as const;
const ReasoningCapability = z
.object({
supported: z.boolean().optional(),
toggle: z.boolean().nullish().transform((value) => value ?? undefined),
effort: z.array(z.enum(REASONING_EFFORTS)).nullish().transform((value) => value ?? undefined),
effort: z.array(z.string()).nullish().transform((value) => value ?? undefined),
budget_tokens: z
.object({ min: z.number().optional(), max: z.number().optional() })
.nullish()
@@ -162,6 +160,15 @@ function modalities(values: string[] | undefined, fallback: Modality[]): Modalit
return [...new Set(result.length > 0 ? result : fallback)];
}
// models.dev's reasoning_options effort enum (schema.ts ReasoningEffortValue).
// Guarding against it means an unexpected upstream value is dropped instead of
// silently producing a TOML that fails `validate`.
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] as const;
type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
function isReasoningEffort(value: string): value is ReasoningEffort {
return (REASONING_EFFORTS as readonly string[]).includes(value);
}
// Project CrossModel's capabilities.reasoning onto models.dev reasoning_options.
// reasoning absent -> undefined (non-reasoning model; option omitted)
// reasoning === {} -> [] (model reasons, no verified user-selectable control)
@@ -172,7 +179,8 @@ function reasoningOptions(model: CrossModelModel): SyncedModel["reasoning_option
const options: NonNullable<SyncedModel["reasoning_options"]> = [];
if (reasoning.toggle === true) options.push({ type: "toggle" });
if (reasoning.effort !== undefined) {
if (reasoning.effort.length > 0) options.push({ type: "effort", values: reasoning.effort });
const values = reasoning.effort.filter(isReasoningEffort);
if (values.length > 0) options.push({ type: "effort", values });
}
if (reasoning.budget_tokens !== undefined) {
const budget: { type: "budget_tokens"; min?: number; max?: number } = { type: "budget_tokens" };
+45 -486
View File
@@ -1,21 +1,16 @@
import { z } from "zod";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { describeModel } from "../../describe.js";
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
import { ReasoningOption } from "../../schema.js";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js";
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
const API_ENDPOINT = "https://api.llmgateway.io/v1/models";
// LLM Gateway names the originating lab in `family`; most already match the
// canonical prefixes understood by resolveModelMetadataBaseModel, and labs
// outside that shared table (e.g. perplexity) resolve through its exact
// `models/` path match without widening the OpenRouter prefix map for every
// other provider. Alias the few that spell the lab differently. (Mirrors
// huggingface's CANONICAL_ORG_PREFIXES.)
// canonical prefixes understood by resolveCanonicalBaseModel. Alias the few that
// spell the lab differently. (Mirrors huggingface's CANONICAL_ORG_PREFIXES.)
const CANONICAL_FAMILY_ALIASES: Record<string, string> = {
grok: "xai",
mistral: "mistralai",
@@ -35,6 +30,10 @@ const Pricing = z.object({
input_cache_write: z.string().optional(),
});
const LLMGatewayProvider = z.object({
reasoning_efforts: z.array(z.string()).optional(),
}).passthrough();
const ReasoningEffortOrder = new Map([
"none",
"minimal",
@@ -55,22 +54,9 @@ export const LLMGatewayModel = z.object({
input_modalities: z.array(z.string()),
output_modalities: z.array(z.string()),
}),
providers: z.array(
z.object({
providerId: z.string().optional(),
vision: z.boolean().optional(),
tools: z.boolean().optional(),
reasoning: z.boolean().optional(),
reasoning_efforts: z.array(
z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"]),
).optional(),
}).passthrough(),
).optional(),
pricing: Pricing,
// Absent for pseudo-models (custom/auto) and some non-text mappings; text
// models always report it.
context_length: z.number().optional(),
max_output: z.number().optional(),
providers: z.array(LLMGatewayProvider),
context_length: z.number(),
supported_parameters: z.array(z.string()),
structured_outputs: z.boolean().optional(),
}).passthrough();
@@ -81,117 +67,31 @@ export const LLMGatewayResponse = z.object({
export type LLMGatewayModel = z.infer<typeof LLMGatewayModel>;
async function fetchLLMGatewayModels(url: string) {
const headers = process.env.LLMGATEWAY_API_KEY
? { Authorization: `Bearer ${process.env.LLMGATEWAY_API_KEY}` }
: undefined;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`LLM Gateway request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
function textOnly(model: LLMGatewayModel) {
const output = model.architecture.output_modalities;
return output.length === 1 && output[0] === "text";
}
// The DevPass (LLM Gateway) provider: the gateway's aggregated catalog of root
// model IDs, auto-routed across upstream providers.
export const llmgateway = {
id: "llmgateway",
name: "DevPass (LLM Gateway)",
name: "LLM Gateway",
modelsDir: "providers/llmgateway/models",
async fetchModels() {
return fetchLLMGatewayModels(API_ENDPOINT);
const headers = process.env.LLMGATEWAY_API_KEY
? { Authorization: `Bearer ${process.env.LLMGATEWAY_API_KEY}` }
: undefined;
const response = await fetch(API_ENDPOINT, { headers });
if (!response.ok) {
throw new Error(`LLM Gateway request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
const data = LLMGatewayResponse.parse(raw).data.filter(textOnly);
// An empty catalog is an upstream fault; syncing it would delete every
// model file, so fail loudly instead.
if (data.length === 0) {
throw new Error("LLM Gateway returned no text models");
}
return data;
return LLMGatewayResponse.parse(raw).data.filter((model) => {
const output = model.architecture.output_modalities;
return output.length === 1 && output[0] === "text";
});
},
translateModel(model, context) {
const translated = buildLLMGatewayModel(model, context.existing(model.id));
if (translated === undefined) {
return undefined;
}
return { id: model.id, model: translated };
},
sourceID(model) {
return model.id;
},
} satisfies SyncProvider<LLMGatewayModel>;
// Every toggle reasoning control requires a leading wire-path comment, and the
// sync runner only carries over headers that already exist on disk. Files this
// sync writes with a toggle get the gateway-wide default; a hand-written
// header on the existing file always wins.
const TOGGLE_HEADER = `# Toggle: $.reasoning_effort = "none" disables thinking; any other accepted
# value (or omitting the field) leaves it on. The gateway maps it to the
# deployment's thinking switch.
# https://docs.llmgateway.io/features/reasoning
`;
function toggleHeader(model: SyncedModel) {
return model.reasoning_options?.some((option) => option.type === "toggle")
? TOGGLE_HEADER
: undefined;
}
// The LLM Gateway provider: one entry per upstream provider mapping, addressed
// the way the gateway accepts provider-pinned requests (`provider/model-id`).
export const llmgatewayProviders = {
id: "llmgateway-providers",
name: "LLM Gateway",
modelsDir: "providers/llmgateway-providers/models",
async fetchModels() {
return fetchLLMGatewayModels(`${API_ENDPOINT}?mapped=true`);
},
parseModels(raw) {
const data = LLMGatewayResponse.parse(raw).data;
// A deployment without the mapped view ignores the query param and returns
// aggregated root IDs (no provider prefix); syncing those here would wipe
// the provider-pinned catalog, so refuse to proceed. An empty response (or
// one left empty after filtering) would silently do the same via the
// delete-missing pass, so it is equally fatal.
if (data.length === 0 || !data.every((model) => model.id.includes("/"))) {
throw new Error("LLM Gateway mapped view unavailable: response is empty or contains unprefixed model ids");
}
// llmgateway/custom is the BYO-model placeholder and llmgateway/auto the
// auto-router; pinning either to a provider is meaningless in this catalog
// (the aggregated llmgateway provider carries `auto`).
const mapped = data.filter((model) => !model.id.startsWith("llmgateway/") && textOnly(model));
if (mapped.length === 0) {
throw new Error("LLM Gateway mapped view returned no text models");
}
// Every mapped entry is one specific provider deployment whose single
// providers[] mapping drives capabilities and reasoning controls. A kept
// entry with zero or several mappings would make the builder silently fall
// back to noisy supported_parameters / sibling defaults, so fail loudly.
const malformed = mapped.filter((model) => model.providers?.length !== 1);
if (malformed.length > 0) {
throw new Error(
`LLM Gateway mapped view returned entries without exactly one provider mapping: ${
malformed.map((model) => model.id).join(", ")
}`,
);
}
return mapped;
},
translateModel(model, context) {
const translated = buildLLMGatewayMappedModel(model, context.existing(model.id));
if (translated === undefined) {
return undefined;
}
return { id: model.id, model: translated, header: toggleHeader(translated) };
},
sourceID(model) {
return model.id;
return {
id: model.id,
model: buildLLMGatewayModel(model, context.existing(model.id)),
};
},
} satisfies SyncProvider<LLMGatewayModel>;
@@ -225,75 +125,12 @@ function modalities(values: string[], fallback: Modality[]): Modality[] {
return [...new Set(result.length > 0 ? result : fallback)];
}
// Modalities as served by a specific deployment: a mapping without vision must
// not carry image/pdf input, regardless of what the model-level architecture
// claims — attachment=false with image input is contradictory.
function deploymentModalities(model: LLMGatewayModel, vision: boolean | undefined) {
const base = defaultModalities(model);
if (vision !== false) {
return base;
}
const input = base.input.filter((value) => value !== "image" && value !== "pdf");
return {
input: input.length > 0 ? input : (["text"] satisfies Modality[]),
output: base.output,
};
}
const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models");
const AGGREGATED_MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers", "llmgateway", "models");
const canonicalOutputLimitByID = new Map<string, number | undefined>();
interface SiblingCuration {
reasoning_options?: SyncedFullModel["reasoning_options"];
interleaved?: SyncedFullModel["interleaved"];
cost_tiers?: NonNullable<SyncedFullModel["cost"]>["tiers"];
}
const siblingCurationByID = new Map<string, SiblingCuration>();
// The aggregated llmgateway catalog curates reasoning controls, the reasoning
// side-channel, and context pricing tiers for the same gateway surface; mapped
// deployments of the same root model reuse them when the deployment does not
// declare its own.
function siblingCuration(rootID: string): SiblingCuration {
let curation = siblingCurationByID.get(rootID);
if (curation === undefined) {
const filePath = path.join(AGGREGATED_MODELS_DIR, `${rootID}.toml`);
const authored = existsSync(filePath)
? Bun.TOML.parse(readFileSync(filePath, "utf8")) as SiblingCuration & {
cost?: { tiers?: NonNullable<SyncedFullModel["cost"]>["tiers"] };
}
: undefined;
curation = {
reasoning_options: authored?.reasoning_options?.length ? authored.reasoning_options : undefined,
interleaved: authored?.interleaved,
cost_tiers: authored?.cost?.tiers,
};
siblingCurationByID.set(rootID, curation);
}
return curation;
}
// Whether the canonical metadata declares limit.output; factored entries can
// only omit their own output override when the base has one to inherit.
function canonicalOutputLimit(modelID: string) {
if (!canonicalOutputLimitByID.has(modelID)) {
const filePath = path.join(MODELS_DIR, `${modelID}.toml`);
const metadata = existsSync(filePath)
? Bun.TOML.parse(readFileSync(filePath, "utf8")) as { limit?: { output?: number } }
: undefined;
canonicalOutputLimitByID.set(modelID, metadata?.limit?.output);
}
return canonicalOutputLimitByID.get(modelID);
}
function resolveLLMGatewayBaseModel(model: LLMGatewayModel, modelID = model.id) {
const alias = BASE_MODEL_ALIASES[modelID];
function resolveLLMGatewayBaseModel(model: LLMGatewayModel) {
const alias = BASE_MODEL_ALIASES[model.id];
if (alias !== undefined) return alias;
if (model.family === undefined) return undefined;
const prefix = CANONICAL_FAMILY_ALIASES[model.family] ?? model.family;
return resolveModelMetadataBaseModel(`${prefix}/${modelID}`);
return resolveCanonicalBaseModel(`${prefix}/${model.id}`);
}
function inferFamily(model: LLMGatewayModel, name: string) {
@@ -315,19 +152,15 @@ function inferFamily(model: LLMGatewayModel, name: string) {
export function buildLLMGatewayModel(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedModel | undefined {
): SyncedModel {
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
const reasoning = model.supported_parameters.includes("reasoning")
|| model.supported_parameters.includes("include_reasoning");
const reasoningOptions = llmGatewayReasoningOptions(model, existing);
const reported = model.context_length ?? 0;
// A missing/zero context must never be authored as limit.context = 0:
// factored entries leave it unset and inherit the base, and unfactored
// creates are skipped entirely. An authored 0 on the existing file is
// equally unusable and must not be re-stamped.
const servedContext = reported > 0 ? reported : undefined;
const context = servedContext ?? (existing?.limit?.context || undefined);
const context = model.context_length > 0
? model.context_length
: existing?.limit?.context ?? model.context_length;
// The gateway is authoritative for the volatile, gateway-specific data — cost,
// served limits, and explicitly advertised reasoning efforts. Its
@@ -346,24 +179,15 @@ export function buildLLMGatewayModel(
tiers: existing?.cost?.tiers,
}
: existing?.cost;
// Authored limits carry only known-positive values — never the zero/absent
// `reported` fallback.
const limit = context !== undefined
? {
context,
input: existing?.limit?.input,
output: (existing?.limit?.output || undefined) ?? context,
}
: undefined;
const limit = {
context,
input: existing?.limit?.input,
output: existing?.limit?.output ?? context,
};
// Existing factored model: refresh cost + limit, keep every authored override
// as-is (undefined fields keep inheriting the base model).
if (existing?.base_model !== undefined) {
const factoredLimit = {
context,
input: existing.limit?.input,
output: existing.limit?.output ?? context,
};
return factorBaseModel(
existing.base_model,
{
@@ -376,7 +200,7 @@ export function buildLLMGatewayModel(
tool_call: existing.tool_call,
structured_output: existing.structured_output,
open_weights: existing.open_weights,
limit: factoredLimit,
limit,
modalities: existing.modalities,
}),
reasoning: existing.reasoning,
@@ -388,22 +212,16 @@ export function buildLLMGatewayModel(
interleaved: existing.interleaved,
knowledge: existing.knowledge,
modalities: existing.modalities,
limit: factoredLimit,
limit,
cost,
},
factoredLimit,
limit,
existing.base_model_omit,
);
}
// Existing full model: refresh cost + limit, preserve curated metadata.
if (existing !== undefined) {
// With no usable context from the API or the file there is nothing valid
// to author, and skipping would hand the file to the delete-missing pass —
// fail loudly rather than write limit.context = 0.
if (limit === undefined) {
throw new Error(`LLM Gateway entry ${model.id} has no usable context to author`);
}
return {
name: existing.name ?? model.name,
description: existing.description ?? describeModel({
@@ -453,12 +271,7 @@ export function buildLLMGatewayModel(
}
// Brand-new model: best-effort translation from the gateway. Capability and
// modality data are unreliable here and should be hand-reviewed. Without a
// positive served context there is nothing usable to author, so skip.
if (servedContext === undefined) {
return undefined;
}
const createdLimit = limit ?? { context: servedContext, input: undefined, output: servedContext };
// modality data are unreliable here and should be hand-reviewed.
const { input, output } = defaultModalities(model);
return {
name: model.name,
@@ -471,7 +284,7 @@ export function buildLLMGatewayModel(
|| model.supported_parameters.includes("tool_choice"),
structured_output: model.structured_outputs ?? false,
open_weights: false,
limit: createdLimit,
limit,
modalities: { input, output },
}),
family: inferFamily(model, model.name),
@@ -486,261 +299,7 @@ export function buildLLMGatewayModel(
structured_output: model.structured_outputs ?? false,
open_weights: false,
cost,
limit: createdLimit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
export function buildLLMGatewayMappedModel(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedModel | undefined {
// Mapped entries carry exactly one provider mapping; its capability flags
// describe that specific deployment, unlike the aggregated view where
// supported_parameters are too noisy to trust.
const mapping = model.providers?.[0];
const rootID = model.id.split("/").slice(1).join("/");
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
// The mapping's flag stays authoritative on resyncs too, so the written
// reasoning boolean and the reasoning_options derived from it always move
// together; prior curation only fills in when the mapping is silent, then
// the noisy supported_parameters signal as a last resort.
const reasoning = mapping?.reasoning
?? existing?.reasoning
?? (model.supported_parameters.includes("reasoning")
|| model.supported_parameters.includes("include_reasoning"));
// The exact reasoning_effort values this deployment accepts. A deployment
// whose only accepted effort is "none" exposes a plain on/off switch (the
// gateway honours it through the thinking toggle), not effort tiers.
const deploymentOptions = mapping?.reasoning_efforts?.length
? mapping.reasoning_efforts.length === 1 && mapping.reasoning_efforts[0] === "none"
? [{ type: "toggle" as const }]
: [{ type: "effort" as const, values: mapping.reasoning_efforts }]
: undefined;
// Deployment-declared efforts own the effort/toggle surface; curation falls
// back from non-empty options on this file to the aggregated llmgateway
// catalog's controls for the same root model on the same gateway surface.
// Curated non-effort controls (e.g. budget_tokens for $.reasoning.max_tokens,
// which this host serves regardless of the effort list) survive alongside
// deployment efforts instead of being wiped by them. A curated [] counts as
// unknown so a bad first stamp is not sticky. Non-reasoning deployments
// carry none; the same applies to the interleaved reasoning side-channel.
const sibling = siblingCuration(rootID);
const curatedOptions = (existing?.reasoning_options?.length ? existing.reasoning_options : undefined)
?? sibling.reasoning_options;
const reasoningOptions = reasoning
? deploymentOptions !== undefined
? [
...(curatedOptions ?? []).filter((option) => option.type !== "effort" && option.type !== "toggle"),
...deploymentOptions,
]
: curatedOptions
: undefined;
const interleaved = reasoning
? existing?.interleaved ?? sibling.interleaved
: undefined;
const reported = model.context_length ?? 0;
// Same zero-context rule as the aggregated builder: never author 0, inherit
// on factored entries, skip unfactored creates. An authored 0 on the
// existing file is equally unusable.
const servedContext = reported > 0 ? reported : undefined;
const context = servedContext ?? (existing?.limit?.context || undefined);
const cost = prompt !== undefined && completion !== undefined
? {
input: prompt,
output: completion,
reasoning: reasoning ? nonZeroPrice(model.pricing.internal_reasoning) ?? existing?.cost?.reasoning : existing?.cost?.reasoning,
cache_read: nonZeroPrice(model.pricing.input_cache_read) ?? existing?.cost?.cache_read,
cache_write: nonZeroPrice(model.pricing.input_cache_write) ?? existing?.cost?.cache_write,
// The gateway API does not expose context pricing tiers, so authored
// tiers stick and new files seed from the aggregated sibling's curated
// tiers rather than silently under-stating long-context pricing.
tiers: existing?.cost?.tiers ?? sibling.cost_tiers,
}
: existing?.cost;
// The gateway's max_output is the deployment's real served limit, so it wins
// over inherited/authored values, unlike the aggregated view.
const servedOutput = (model.max_output || undefined) ?? (existing?.limit?.output || undefined);
// Authored limits carry only known-positive values — never the zero/absent
// `reported` fallback.
const limit = context !== undefined
? {
context,
input: existing?.limit?.input,
output: servedOutput ?? context,
}
: undefined;
// Existing factored model: refresh cost + limit, keep every authored override
// as-is. Unlike the aggregated provider, the name override must be carried
// forward: mapped names disambiguate deployments of the same model (e.g.
// "GPT-5.5 (Azure)" vs "GPT-5.5 (OpenAI)") and must not collapse back to the
// base metadata name.
if (existing?.base_model !== undefined) {
// Mirror the brand-new factored path: without a served or authored output,
// keep inheriting the base's output rather than stamping context over it.
const factoredLimit = {
context,
input: existing.limit?.input,
output: servedOutput ?? (canonicalOutputLimit(existing.base_model) !== undefined ? undefined : context),
};
// Deployment capability flags keep their create-path authority on
// resyncs: a mapping that gains or loses reasoning/vision/tools/structured
// outputs realigns the written flags together with the reasoning_options
// computed from them, instead of freezing stale curation forever.
return factorBaseModel(
existing.base_model,
{
name: existing.name ?? model.name,
attachment: mapping?.vision ?? existing.attachment,
// No describeModel fallback: synthesizing a description here would
// stamp a sticky generic override on every name-pinned factored entry;
// leaving it unset keeps inheriting the lab text from the base.
description: existing.description,
reasoning: mapping?.reasoning ?? existing.reasoning,
reasoning_options: reasoningOptions,
temperature: existing.temperature,
tool_call: mapping?.tools ?? existing.tool_call,
structured_output: model.structured_outputs ?? existing.structured_output,
status: existing.status,
interleaved,
knowledge: existing.knowledge,
// Vision realigns modalities in both directions: false strips
// image/pdf, true clears any stale stripped override so the base's
// richer inputs inherit again; only a silent mapping keeps curation.
modalities: mapping?.vision === undefined
? existing.modalities
: mapping.vision
? undefined
: deploymentModalities(model, false),
limit: factoredLimit,
cost,
},
factoredLimit,
existing.base_model_omit,
);
}
// Existing full model: refresh cost + limit, preserve curated metadata.
// Capability flags follow the same rule as the factored path above: the
// deployment mapping wins, curation fills the gaps.
if (existing !== undefined) {
// With no usable context from the API or the file there is nothing valid
// to author, and skipping would hand the file to the delete-missing pass —
// fail loudly rather than write limit.context = 0.
if (limit === undefined) {
throw new Error(`LLM Gateway mapped entry ${model.id} has no usable context to author`);
}
const resolved = {
attachment: mapping?.vision ?? existing.attachment ?? false,
tool_call: mapping?.tools ?? existing.tool_call ?? false,
structured_output: model.structured_outputs ?? existing.structured_output,
// Same bidirectional vision rule as the factored path; with no base to
// inherit from, a declared vision recomputes from the served
// architecture instead of clearing.
modalities: mapping?.vision === undefined
? existing.modalities ?? deploymentModalities(model, undefined)
: deploymentModalities(model, mapping.vision),
};
return {
name: existing.name ?? model.name,
description: existing.description ?? describeModel({
id: model.id,
name: existing.name ?? model.name,
family: existing.family,
reasoning,
tool_call: resolved.tool_call,
structured_output: resolved.structured_output,
open_weights: existing.open_weights,
limit,
modalities: resolved.modalities,
}),
family: existing.family,
release_date: existing.release_date ?? dateFromTimestamp(model.created),
last_updated: existing.last_updated ?? dateFromTimestamp(model.created),
attachment: resolved.attachment,
reasoning,
reasoning_options: reasoningOptions,
temperature: existing.temperature ?? false,
tool_call: resolved.tool_call,
structured_output: resolved.structured_output,
knowledge: existing.knowledge,
open_weights: existing.open_weights ?? false,
status: existing.status,
interleaved,
cost,
limit,
modalities: resolved.modalities,
} satisfies SyncedFullModel;
}
// Brand-new model with a reviewed metadata entry: factor against the
// canonical base. The mapped ID is `serving-provider/model-id` and the
// serving provider is unrelated to the originating lab, so resolve the base
// from the root model ID + family, and keep the disambiguating name. The
// mapping's own capability flags describe this specific deployment, so they
// go in as overrides (factorBaseModel drops the ones equal to the base).
const canonical = resolveLLMGatewayBaseModel(model, rootID);
if (canonical !== undefined) {
const factoredLimit = {
context,
input: undefined,
// Without a served limit, inherit the base's output; only fall back to
// context when the base declares none (output is required downstream).
output: model.max_output ?? (canonicalOutputLimit(canonical) !== undefined ? undefined : context),
};
return factorBaseModel(canonical, {
name: model.name,
attachment: mapping?.vision,
reasoning: mapping?.reasoning,
reasoning_options: reasoningOptions,
interleaved,
tool_call: mapping?.tools,
structured_output: model.structured_outputs,
// A deployment without vision must not inherit image/pdf inputs from
// the base — attachment=false with image input is contradictory.
modalities: mapping?.vision === false ? deploymentModalities(model, false) : undefined,
limit: factoredLimit,
cost,
}, factoredLimit);
}
// Brand-new model without metadata: best-effort translation. The mapping's
// own capability flags are reliable here; modalities mirror the mapping too.
// Without a positive served context there is nothing usable to author.
if (servedContext === undefined) {
return undefined;
}
const createdLimit = limit ?? { context: servedContext, input: undefined, output: servedOutput ?? servedContext };
const { input, output } = deploymentModalities(model, mapping?.vision);
return {
name: model.name,
description: describeModel({
id: model.id,
name: model.name,
family: inferFamily(model, model.name),
reasoning,
tool_call: mapping?.tools ?? false,
structured_output: model.structured_outputs ?? false,
open_weights: false,
limit: createdLimit,
modalities: { input, output },
}),
family: inferFamily(model, model.name),
release_date: dateFromTimestamp(model.created),
last_updated: dateFromTimestamp(model.created),
attachment: mapping?.vision ?? input.some((value) => value !== "text"),
reasoning,
reasoning_options: reasoningOptions,
interleaved,
temperature: model.supported_parameters.includes("temperature"),
tool_call: mapping?.tools ?? false,
structured_output: model.structured_outputs ?? false,
open_weights: false,
cost,
limit: createdLimit,
limit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
@@ -749,7 +308,7 @@ function llmGatewayReasoningOptions(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedFullModel["reasoning_options"] {
const advertised = new Set((model.providers ?? []).flatMap((provider) => provider.reasoning_efforts ?? []));
const advertised = new Set(model.providers.flatMap((provider) => provider.reasoning_efforts ?? []));
if (advertised.size === 0) return undefined;
const efforts = [...advertised].sort((a, b) => {
+3 -15
View File
@@ -70,15 +70,13 @@ export const vercel = {
},
translateModel(model, context) {
const existing = context.existing(model.id);
const routeBase = freeRouteBase(model.id);
const baseModel = existing?.base_model ?? resolveVercelBaseModel(model.id);
const inherited = routeBase === undefined ? undefined : context.existing(routeBase);
const baseModel = existing?.base_model ?? resolveCanonicalBaseModel(model.id);
return {
id: model.id,
model: buildVercelModel(
model,
existing,
inherited ?? (baseModel === undefined || baseModel === model.id ? undefined : context.existing(baseModel)),
baseModel === undefined || baseModel === model.id ? undefined : context.existing(baseModel),
),
};
},
@@ -180,7 +178,7 @@ export function buildVercelModel(
},
};
const baseModel = existing?.base_model ?? resolveVercelBaseModel(model.id);
const baseModel = existing?.base_model ?? resolveCanonicalBaseModel(model.id);
if (baseModel === undefined) return synced;
return factorBaseModel(baseModel, {
@@ -201,16 +199,6 @@ export function buildVercelModel(
}, synced.limit, existing?.base_model_omit);
}
function resolveVercelBaseModel(modelID: string) {
const routeBase = freeRouteBase(modelID);
return resolveCanonicalBaseModel(modelID)
?? (routeBase === undefined ? undefined : resolveCanonicalBaseModel(routeBase));
}
function freeRouteBase(modelID: string) {
return modelID.endsWith("-free") ? modelID.slice(0, -"-free".length) : undefined;
}
function dateFromTimestamp(timestamp: number) {
return new Date(timestamp * 1000).toISOString().slice(0, 10);
}
+1 -1
View File
@@ -82,7 +82,7 @@ test("does not inspect deleted models", async () => {
});
test("allows reviewed providers with explicit reasoning options", async () => {
for (const provider of ["crossmodel", "edenai", "empiriolabs", "hyper", "kilo", "llmgateway", "llmgateway-providers", "merge-gateway", "nano-gpt", "openrouter", "venice"]) {
for (const provider of ["edenai", "empiriolabs", "hyper", "kilo", "llmgateway", "merge-gateway", "nano-gpt", "openrouter", "venice"]) {
const decision = await classifyAutoMerge(
[{ status: "updated", path: `providers/${provider}/models/reasoner.toml` }],
async () => fullModel(true, 'reasoning_options = [{ type = "toggle" }]'),
+2 -502
View File
@@ -10,7 +10,7 @@ import {
parseAnthropicPricing,
type AnthropicModel,
} from "../src/sync/providers/anthropic.js";
import { buildCortecsModel, cortecs, type CortecsModel } from "../src/sync/providers/cortecs.js";
import { buildCortecsModel, type CortecsModel } from "../src/sync/providers/cortecs.js";
import {
buildCrossModel,
CrossModelResponse,
@@ -56,13 +56,7 @@ import {
resolveCanonicalBaseModel,
type OpenRouterModel,
} from "../src/sync/providers/openrouter.js";
import {
buildLLMGatewayMappedModel,
buildLLMGatewayModel,
llmgateway,
llmgatewayProviders,
type LLMGatewayModel,
} from "../src/sync/providers/llmgateway.js";
import { buildLLMGatewayModel, type LLMGatewayModel } from "../src/sync/providers/llmgateway.js";
import {
buildMergeGatewayModel,
fetchMergeGatewayModels,
@@ -473,46 +467,6 @@ test("parses CrossModel's nullable reasoning controls", () => {
});
});
test("syncs CrossModel's explicit reasoning controls", () => {
const model = buildCrossModel(
crossModelModel({
capabilities: {
json: true,
reasoning: {
supported: true,
toggle: true,
effort: ["low", "high", "max"],
budget_tokens: { min: 1_024, max: 32_000 },
},
},
}),
undefined,
);
expect(model).toMatchObject({
reasoning_options: [
{ type: "toggle" },
{ type: "effort", values: ["low", "high", "max"] },
{ type: "budget_tokens", min: 1_024, max: 32_000 },
],
});
});
test("rejects unknown CrossModel reasoning efforts", () => {
expect(() =>
CrossModelResponse.parse({
data: [
{
...crossModelModel(),
capabilities: {
reasoning: { supported: true, effort: ["unexpected"] },
},
},
],
})
).toThrow();
});
test("syncs NanoGPT's verified reasoning, pricing, limits, and open-weight metadata", () => {
const model = buildNanoGptModel(nanoGptModel({
pricing: {
@@ -2681,27 +2635,6 @@ test("defaults new reasoning models to empty reasoning options", () => {
});
});
test("inherits base reasoning options instead of stamping empty ones", () => {
expect(preserveReasoningOptions({ reasoning: true }, undefined, undefined, [{ type: "toggle" }]))
.toEqual({ reasoning: true });
});
test("normalizes Cortecs file modalities to pdf", () => {
const [model] = cortecs.parseModels({
object: "list",
data: [{
id: "document-model",
created: 1_775_088_000,
pricing: { currency: "EUR", input_token: 1, output_token: 2 },
context_size: 65_536,
input_modalities: ["text", "file"],
output_modalities: ["text"],
}],
});
expect(model.input_modalities).toEqual(["text", "pdf"]);
});
test("preserves authored Cortecs reasoning options missing from the API", () => {
const model: CortecsModel = {
id: "deepseek-v4-flash-0731",
@@ -3032,52 +2965,6 @@ test("factors aliased LLM Gateway routes against canonical metadata", () => {
});
});
test("factors mapped LLM Gateway entries against the root model metadata", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel(), undefined);
expect(model).toEqual({
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
structured_output: true,
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
},
});
});
test("applies deployment capability flags on mapped factored entries", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
providers: [{ providerId: "anthropic", vision: false, tools: false, reasoning: false }],
architecture: { input_modalities: ["text"], output_modalities: ["text"] },
max_output: 64_000,
}), undefined);
expect(model).toEqual({
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
attachment: false,
reasoning: false,
tool_call: false,
structured_output: true,
modalities: {
input: ["text"],
},
limit: {
output: 64_000,
},
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
},
});
});
test("factors Grok LLM Gateway routes against xAI metadata", () => {
const model = buildLLMGatewayModel(llmGatewayModel({
id: "grok-4-6",
@@ -3101,339 +2988,6 @@ test("factors Grok LLM Gateway routes against xAI metadata", () => {
});
});
test("prefers the gateway max_output over authored output on mapped resyncs", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({ max_output: 32_000 }), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
description: "Claude Fable 5 served by Anthropic",
limit: { output: 64_000 },
});
expect(model).toEqual({
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
description: "Claude Fable 5 served by Anthropic",
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
structured_output: true,
limit: {
output: 32_000,
},
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
},
});
});
test("translates a none-only effort list into a reasoning toggle", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
providers: [{ providerId: "anthropic", vision: true, tools: true, reasoning: true, reasoning_efforts: ["none"] }],
}), undefined);
expect(model).toMatchObject({
base_model: "anthropic/claude-fable-5",
reasoning_options: [{ type: "toggle" }],
});
});
test("realigns capability flags from the mapping on mapped factored resyncs", () => {
// The deployment dropped reasoning and gained tools since the file was
// written: the resync must move the booleans and the reasoning controls
// together instead of clearing options under a frozen reasoning = true.
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
providers: [{ providerId: "anthropic", vision: true, tools: true, reasoning: false }],
}), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
reasoning: true,
reasoning_options: [{ type: "toggle" }],
tool_call: false,
});
expect(model).toMatchObject({ reasoning: false });
expect(model!.reasoning_options).toBeUndefined();
// Realigned to the mapping and now equal to the base, the stale
// tool_call = false override is dropped and inherits the base again.
expect(model!.tool_call).toBeUndefined();
});
test("restores image input when vision returns on mapped resyncs", () => {
// The file was written while the deployment had no vision (text-only
// stripped modalities); vision is back, so the stale override must clear.
const factored = buildLLMGatewayMappedModel(llmGatewayMappedModel(), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
attachment: false,
modalities: { input: ["text"] },
});
expect(factored!.modalities).toBeUndefined();
expect(factored!.attachment).toBeUndefined();
const full = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "acme/mystery-model",
name: "Mystery Model (Acme)",
family: undefined,
providers: [{ providerId: "acme", vision: true, tools: true, reasoning: false }],
}), {
name: "Mystery Model (Acme)",
attachment: false,
modalities: { input: ["text"], output: ["text"] },
});
expect(full).toMatchObject({
attachment: true,
modalities: { input: ["text", "image"], output: ["text"] },
});
});
test("never synthesizes a description on mapped factored resyncs", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel(), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
});
// An unset description must keep inheriting the base's lab text instead of
// being stamped with a sticky synthesized override on the first resync.
expect(model).toBeDefined();
expect(model!.description).toBeUndefined();
});
test("authors the toggle wire-path header on mapped sync creates", () => {
const context = { existing: () => undefined, authored: () => undefined };
const toggle = llmgatewayProviders.translateModel(llmGatewayMappedModel({
providers: [{ providerId: "anthropic", vision: true, tools: true, reasoning: true, reasoning_efforts: ["none"] }],
}), context);
expect(toggle?.header).toStartWith("# Toggle: $.reasoning_effort");
const effort = llmgatewayProviders.translateModel(llmGatewayMappedModel(), context);
expect(effort?.model).toMatchObject({
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
});
expect(effort?.header).toBeUndefined();
});
test("keeps inheriting base output on factored resyncs without max_output", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({ max_output: undefined }), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
description: "Claude Fable 5 served by Anthropic",
});
expect(model).toEqual({
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
description: "Claude Fable 5 served by Anthropic",
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
structured_output: true,
cost: {
input: 10,
output: 50,
cache_read: 1,
cache_write: 12.5,
},
});
});
test("skips unfactorable LLM Gateway creates without a served context", () => {
// Unknown family, so no canonical base to inherit a context from.
const mapped = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "acme/mystery-model",
name: "Mystery Model (Acme)",
family: undefined,
context_length: undefined,
}), undefined);
expect(mapped).toBeUndefined();
const aggregated = buildLLMGatewayModel(llmGatewayModel({
id: "mystery-model",
name: "Mystery Model",
family: undefined,
context_length: undefined,
}), undefined);
expect(aggregated).toBeUndefined();
});
test("keeps curated budget controls under deployment efforts", () => {
// Deployment efforts own only the effort/toggle surface: the hand-authored
// budget_tokens control (this host's $.reasoning.max_tokens path) survives
// the resync, while the stale effort list is replaced.
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel(), {
base_model: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
reasoning_options: [
{ type: "effort", values: ["low", "high"] },
{ type: "budget_tokens", min: 1_024, max: 63_999 },
],
});
expect(model!.reasoning_options).toEqual([
{ type: "budget_tokens", min: 1_024, max: 63_999 },
{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] },
]);
// Same merge on creates, with the budget coming from the aggregated
// sibling's curation for the same root model.
const seeded = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "anthropic/claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Anthropic)",
}), undefined);
expect(seeded!.reasoning_options).toEqual([
{ type: "budget_tokens", min: 1_024, max: 63_999 },
{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] },
]);
});
test("seeds context pricing tiers from the aggregated sibling on creates", () => {
// The gateway API carries no tier pricing; without the sibling's curated
// tiers the bulk sync would author tiered models at flat long-context rates.
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "openai/gpt-5.5",
name: "GPT-5.5 (OpenAI)",
family: "openai",
}), undefined);
expect(model!.cost?.tiers).toEqual([
{ tier: { type: "context", size: 272_000 }, input: 10, output: 45, cache_read: 1 },
]);
});
test("factors perplexity entries without widening the shared prefix map", () => {
// The perplexity family resolves through resolveModelMetadataBaseModel's
// exact models/ path match; CANONICAL_PROVIDER_PREFIXES stays untouched so
// other hosts' standalone perplexity files keep their current behavior.
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "perplexity/sonar-pro",
name: "Sonar Pro (Perplexity)",
family: "perplexity",
}), undefined);
expect(model).toMatchObject({ base_model: "perplexity/sonar-pro" });
});
test("refuses to author a zero context on full LLM Gateway resyncs", () => {
// Existing full rows (no base to inherit from) with nothing usable from the
// API or the file must fail loudly instead of being rewritten with
// limit.context = 0.
expect(() => buildLLMGatewayMappedModel(llmGatewayMappedModel({
context_length: undefined,
max_output: undefined,
}), {
name: "Claude Fable 5 (Anthropic)",
})).toThrow("no usable context");
// An authored 0 on disk is as unusable as an absent context.
expect(() => buildLLMGatewayMappedModel(llmGatewayMappedModel({
context_length: 0,
max_output: undefined,
}), {
name: "Claude Fable 5 (Anthropic)",
limit: { context: 0 },
})).toThrow("no usable context");
expect(() => buildLLMGatewayModel(llmGatewayModel({
context_length: undefined,
}), {
name: "Claude Fable 5",
})).toThrow("no usable context");
});
test("leaves context unset on mapped factored creates without a served context", () => {
const model = buildLLMGatewayMappedModel(llmGatewayMappedModel({
context_length: undefined,
max_output: undefined,
}), undefined);
// Everything limit-related inherits from the base; no zero is authored.
expect(model).toBeDefined();
expect("limit" in model!).toBe(false);
});
test("strips image input when the deployment has no vision", () => {
// The model-level architecture still claims image input; the deployment
// flag must win on both the factored and the unfactored path.
const factored = buildLLMGatewayMappedModel(llmGatewayMappedModel({
providers: [{ providerId: "anthropic", vision: false, tools: true, reasoning: false }],
}), undefined);
expect(factored).toMatchObject({
base_model: "anthropic/claude-fable-5",
attachment: false,
modalities: { input: ["text"] },
});
const full = buildLLMGatewayMappedModel(llmGatewayMappedModel({
id: "acme/mystery-model",
name: "Mystery Model (Acme)",
family: undefined,
providers: [{ providerId: "acme", vision: false, tools: true, reasoning: false }],
}), undefined);
expect(full).toMatchObject({
attachment: false,
modalities: { input: ["text"], output: ["text"] },
});
});
test("refuses empty responses in both LLM Gateway syncs", () => {
expect(() => llmgateway.parseModels({ data: [] })).toThrow("no text models");
expect(() => llmgatewayProviders.parseModels({ data: [] })).toThrow("mapped view unavailable");
});
test("refuses aggregated responses in the mapped LLM Gateway sync", () => {
expect(() => llmgatewayProviders.parseModels({ data: [llmGatewayModel()] }))
.toThrow("mapped view unavailable");
});
test("filters pseudo and non-text entries from the mapped LLM Gateway sync", () => {
const parsed = llmgatewayProviders.parseModels({
data: [
llmGatewayMappedModel(),
llmGatewayMappedModel({ id: "llmgateway/auto", name: "Auto Route (LLM Gateway)" }),
llmGatewayMappedModel({
id: "openai/sora-2",
name: "Sora 2 (OpenAI)",
architecture: { input_modalities: ["text"], output_modalities: ["video"] },
}),
],
});
expect(parsed.map((model) => model.id)).toEqual(["anthropic/claude-fable-5"]);
});
test("refuses mapped LLM Gateway entries without exactly one provider mapping", () => {
expect(() => llmgatewayProviders.parseModels({
data: [llmGatewayMappedModel({ providers: undefined })],
})).toThrow("without exactly one provider mapping");
expect(() => llmgatewayProviders.parseModels({
data: [llmGatewayMappedModel({ providers: [] })],
})).toThrow("without exactly one provider mapping");
expect(() => llmgatewayProviders.parseModels({
data: [
llmGatewayMappedModel(),
llmGatewayMappedModel({
id: "azure/gpt-5.5",
name: "GPT-5.5 (Azure)",
providers: [{ providerId: "azure" }, { providerId: "openai" }],
}),
],
})).toThrow("azure/gpt-5.5");
// Entries the sync drops anyway (pseudo-models, non-text) may lack a
// mapping without tripping the guard.
const parsed = llmgatewayProviders.parseModels({
data: [
llmGatewayMappedModel(),
llmGatewayMappedModel({
id: "llmgateway/auto",
name: "Auto Route (LLM Gateway)",
providers: undefined,
}),
],
});
expect(parsed.map((model) => model.id)).toEqual(["anthropic/claude-fable-5"]);
});
// Ensures catalog pagination preserves authentication and returns every page.
test("fetches every page of the Merge Gateway catalog", async () => {
const requests: string[] = [];
@@ -3987,44 +3541,6 @@ test("Vercel factored models inherit temperature from base metadata", () => {
expect(synced).not.toHaveProperty("temperature");
});
test("Vercel free routes factor onto the canonical non-free model", () => {
const [model] = vercel.parseModels({
data: [{
id: "zai/glm-4.6v-flash-free",
name: "GLM-4.6V-Flash (Free)",
created: 1_765_152_000,
released: 1_765_152_000,
context_window: 128_000,
max_tokens: 24_000,
type: "language",
tags: ["reasoning", "tool-use", "vision", "file-input"],
pricing: { input: "0", output: "0" },
}],
});
const translated = vercel.translateModel(model!, {
existing(id) {
return id === "zai/glm-4.6v-flash"
? { reasoning_options: [{ type: "toggle" }] }
: undefined;
},
authored() {
return undefined;
},
});
expect(translated?.model).toMatchObject({
base_model: "zhipuai/glm-4.6v-flash",
name: "GLM-4.6V-Flash (Free)",
reasoning_options: [{ type: "toggle" }],
cost: { input: 0, output: 0 },
limit: { output: 24_000 },
modalities: { input: ["text", "image", "pdf"] },
});
expect(translated?.model).not.toHaveProperty("description");
expect(translated?.model).not.toHaveProperty("family");
});
test("Vercel Claude Opus fast variants factor onto base opus metadata", () => {
const [model] = vercel.parseModels({
data: [{
@@ -4303,22 +3819,6 @@ function llmGatewayModel(overrides: Partial<LLMGatewayModel> = {}): LLMGatewayMo
};
}
function llmGatewayMappedModel(overrides: Partial<LLMGatewayModel> = {}): LLMGatewayModel {
return llmGatewayModel({
id: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
providers: [{
providerId: "anthropic",
vision: true,
tools: true,
reasoning: true,
reasoning_efforts: ["low", "medium", "high", "xhigh", "max"],
}],
max_output: 128_000,
...overrides,
});
}
function mergeGatewayVendor(
overrides: Partial<MergeGatewayModel["vendors"][string]> = {},
): MergeGatewayModel["vendors"][string] {
+1 -4
View File
@@ -68,10 +68,7 @@ export interface CostTier extends Cost {
/** Pricing for a provider's model, including context-size tiers. */
export interface ModelCost extends Cost {
/**
* Legacy compatibility field for context-tier pricing.
* @deprecated Use `tiers` to read the exact context threshold.
*/
/** Legacy compatibility field: pricing applied beyond 200K context. Prefer `tiers`. */
context_over_200k?: Cost
/** Context-size-based pricing tiers. */
tiers?: CostTier[]
-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" role="img">
<title>AgentRouter</title>
<path d="M11.8 5C7.8 4.2 3 9.6 4.2 15.6c.4 3 3 4.6 6.6 3-1.8-3-2.6-7.8 1-13.6Zm.4 14c4 .8 8.8-4.6 7.6-10.6-.4-3-3-4.6-6.6-3 1.8 3 2.6 7.8-1 13.6ZM12 9.5c.2 1.5 1 2.3 2.5 2.5-1.5.2-2.3 1-2.5 2.5-.2-1.5-1-2.3-2.5-2.5 1.5-.2 2.3-1 2.5-2.5Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 370 B

@@ -1,8 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "anthropic/claude-opus-4-8"
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[provider]
npm = "@ai-sdk/anthropic"
api = "https://agentrouter.org/v1"
@@ -1,8 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "anthropic/claude-opus-5"
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[provider]
npm = "@ai-sdk/anthropic"
api = "https://agentrouter.org/v1"
@@ -1,4 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "openai/gpt-5.6-sol"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
-5
View File
@@ -1,5 +0,0 @@
name = "AgentRouter"
env = ["AGENTROUTER_API_KEY"]
npm = "@ai-sdk/openai-compatible"
api = "https://agentrouter.org/v1"
doc = "https://agentrouter.org/docs/opencode.html"
@@ -1,13 +0,0 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
base_model = "deepseek/deepseek-v4-flash-0731"
reasoning_options = [{ type = "effort", values = ["none", "low", "high", "max"] }]
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.20
output = 0.50
cache_read = 0.10
[limit]
context = 1_048_576
output = 81_920
+1
View File
@@ -2,6 +2,7 @@
# AKI.IO uses the reasoning_option 'none' to disable reasoning
base_model = "google/gemma-4-26b-a4b-it"
attachment = false
reasoning = true
reasoning_options = [{ type = "effort", values = ["none", "high"] }]
[cost]
@@ -4,11 +4,9 @@ base_model = "moonshotai/kimi-k2.7-code"
attachment = false
reasoning_options = []
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.86
output = 3.00
cache_read = 0.18
[limit]
context = 262_144
@@ -0,0 +1,12 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
base_model = "minimax/MiniMax-M2.5"
attachment = false
reasoning_options = []
[cost]
input = 0.25
output = 1.20
[limit]
context = 196_608
output = 65_536
-14
View File
@@ -1,14 +0,0 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
# AKI.IO uses the reasoning_option 'none' to disable reasoning
base_model = "alibaba/qwen3.8-27b"
attachment = false
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "xhigh"] }]
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.30
output = 2.20
cache_read = 0.10
[modalities]
input = ["text", "image"]
@@ -21,8 +21,6 @@ max = 81_920
input = 0.115
output = 0.287
reasoning = 1.147
cache_read = 0.012
cache_write = 0.144
[limit]
context = 1_000_000
@@ -1,14 +0,0 @@
# Source: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-6.html
base_model = "xai/grok-4.6"
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh"] }]
last_updated = "2026-08-18"
[cost]
input = 2.20
output = 6.60
cache_read = 0.55
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
shape = "responses"
@@ -11,9 +11,9 @@ type = "effort"
values = ["high", "max"]
[cost]
input = 0.08
output = 0.18
cache_read = 0.016
input = 0.14
output = 0.28
cache_read = 0.028
cache_write = 0
[limit]
@@ -6,6 +6,7 @@
# https://docs.baseten.co/inference/model-apis/overview
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "deepseek/deepseek-v4-flash-0731"
name = "Deepseek V4 Flash 0731"
[interleaved]
field = "reasoning_content"
@@ -2,6 +2,7 @@
# DeepSeek V4 Pro 0813. Reasoning output is returned in reasoning_content.
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "deepseek/deepseek-v4-pro-0813"
name = "Deepseek V4 Pro 0813"
description = "Flagship DeepSeek model for coding, reasoning, and agentic work"
[interleaved]
@@ -3,6 +3,7 @@
# reasoning-token budget field is documented.
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "deepseek/deepseek-v4-pro"
name = "Deepseek V4 Pro"
[interleaved]
field = "reasoning_content"
@@ -6,9 +6,9 @@ last_updated = "2026-08-16"
reasoning_options = []
[cost]
input = 0.35
output = 2.75
cache_read = 0.03499999999999999
input = 0.45
output = 3.2
cache_read = 0.04499999999999999
[limit]
output = 65_536
@@ -18,3 +18,6 @@ cache_write = 4.078
[limit]
output = 200_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 3.999
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 1.186
[limit]
output = 200_000
[modalities]
input = ["text", "image"]
@@ -17,3 +17,6 @@ cache_write = 6.874
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 6.645
[limit]
output = 200_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 6.645
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 6.797
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -17,3 +17,6 @@ cache_write = 6.797
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -18,3 +18,6 @@ cache_write = 3.624
[limit]
output = 200_000
[modalities]
input = ["text", "image"]
@@ -17,3 +17,6 @@ cache_write = 2.749
[limit]
output = 1_000_000
[modalities]
input = ["text", "image"]
@@ -11,4 +11,4 @@ cache_write = 0.097
output = 1_048_576
[modalities]
input = ["text", "image", "audio", "pdf"]
input = ["text", "image", "audio"]
@@ -11,4 +11,4 @@ cache_write = 0.082
output = 1_048_576
[modalities]
input = ["text", "image", "audio", "pdf"]
input = ["text", "image", "audio"]
@@ -1,8 +1,5 @@
base_model = "google/gemini-3.5-flash-lite"
[[reasoning_options]]
type = "effort"
values = ["minimal", "low", "medium", "high"]
reasoning_options = [{ type = "effort", values = ["minimal", "low", "medium", "high"] }]
[cost]
input = 0.33
@@ -13,4 +10,4 @@ cache_read = 0.033
output = 1_048_576
[modalities]
input = ["text", "image", "audio", "pdf"]
input = ["text", "image", "audio"]
@@ -1,8 +1,5 @@
base_model = "google/gemini-3.7-flash"
[[reasoning_options]]
type = "effort"
values = ["low", "medium", "high"]
reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }]
[cost]
input = 0.75
@@ -14,4 +11,4 @@ cache_write = 0.038
output = 1_048_576
[modalities]
input = ["text", "image", "audio", "pdf"]
input = ["text", "image", "audio"]
@@ -8,3 +8,6 @@ cache_read = 0.134
[limit]
output = 1_047_576
[modalities]
input = ["text", "image"]
@@ -8,6 +8,3 @@ cache_read = 0.056
[limit]
output = 1_047_576
[modalities]
input = ["text", "image", "pdf"]
+3
View File
@@ -9,3 +9,6 @@ cache_read = 0.546
[limit]
output = 1_047_576
[modalities]
input = ["text", "image"]
@@ -8,3 +8,6 @@ cache_read = 0.081
[limit]
output = 128_000
[modalities]
input = ["text", "image"]
+3
View File
@@ -8,3 +8,6 @@ cache_read = 1.33
[limit]
output = 128_000
[modalities]
input = ["text", "image"]
-3
View File
@@ -12,6 +12,3 @@ cache_read = 0.156
[limit]
output = 400_000
[modalities]
input = ["text", "image", "pdf"]
+3
View File
@@ -12,3 +12,6 @@ cache_read = 0.242
[limit]
output = 1_050_000
[modalities]
input = ["text", "image"]
+7 -4
View File
@@ -5,10 +5,13 @@ type = "effort"
values = ["low", "medium", "high"]
[cost]
input = 0.219
output = 1.32
cache_read = 0.022
cache_write = 0.275
input = 1.1
output = 6.599
cache_read = 0.11
cache_write = 1.38
[limit]
output = 1_050_000
[modalities]
input = ["text", "image"]
@@ -12,3 +12,6 @@ cache_write = 6.879
[limit]
output = 1_050_000
[modalities]
input = ["text", "image"]
+7 -4
View File
@@ -5,10 +5,13 @@ type = "effort"
values = ["low", "medium", "high"]
[cost]
input = 2.2
output = 13.199
cache_read = 0.219
cache_write = 2.749
input = 2.749
output = 16.498
cache_read = 0.275
cache_write = 3.437
[limit]
output = 1_050_000
[modalities]
input = ["text", "image"]
+1 -1
View File
@@ -16,4 +16,4 @@ cache_read = 0.193
output = 256_000
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
+1 -1
View File
@@ -10,4 +10,4 @@ output = 3.5
cache_read = 0.201
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
+2 -2
View File
@@ -8,8 +8,8 @@ field = "reasoning_content"
[cost]
input = 0.296
output = 1.186
cache_read = 0.075
output = 1.087
cache_read = 0.03
[limit]
context = 196_680
@@ -2,7 +2,7 @@ name = "mistral-7b-instruct-v0.2"
description = "Mistral 7B Instruct is a compact, 7B parameter model optimized for fast and efficient text and code generation with a 32K token context window."
release_date = "2025-05-26"
last_updated = "2025-05-26"
attachment = true
attachment = false
reasoning = false
temperature = false
tool_call = false
@@ -18,5 +18,5 @@ context = 32_000
output = 32_000
[modalities]
input = ["text", "pdf"]
input = ["text"]
output = ["text"]
@@ -2,7 +2,7 @@ name = "mistral-large-2402"
description = "Mistral Large (24.02) is Mistral AIs most advanced language model, built for complex multilingual reasoning, code generation, and deep text understanding."
release_date = "2025-05-26"
last_updated = "2025-05-26"
attachment = true
attachment = false
reasoning = true
temperature = false
tool_call = true
@@ -19,5 +19,5 @@ context = 32_000
output = 32_000
[modalities]
input = ["text", "pdf"]
input = ["text"]
output = ["text"]
@@ -2,7 +2,7 @@ name = "Mixtral 8x7B Instruct v0.1"
description = "Reasoning model for deliberate analysis, multi-step problem solving, and tool use"
release_date = "2023-12-11"
last_updated = "2023-12-11"
attachment = true
attachment = false
reasoning = true
temperature = true
tool_call = false
@@ -20,5 +20,5 @@ context = 32_000
output = 32_000
[modalities]
input = ["text", "pdf"]
input = ["text"]
output = ["text"]
+1 -1
View File
@@ -19,5 +19,5 @@ context = 1_000_000
output = 1_000_000
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
output = ["text"]
+1 -1
View File
@@ -19,5 +19,5 @@ context = 300_000
output = 300_000
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
output = ["text"]
+1 -1
View File
@@ -21,5 +21,5 @@ context = 300_000
output = 5_000
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
output = ["text"]
@@ -19,5 +19,5 @@ context = 128_000
output = 128_000
[modalities]
input = ["text", "image", "pdf"]
input = ["text", "image"]
output = ["text"]
+16
View File
@@ -0,0 +1,16 @@
base_model = "zhipuai/glm-4.7-flash"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[cost]
input = 0.04
output = 0.3
cache_read = 0.008
cache_write = 0
[limit]
context = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
+20
View File
@@ -0,0 +1,20 @@
base_model = "zhipuai/glm-4.7"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.25
output = 1.1
cache_read = 0.05
cache_write = 0
[limit]
context = 202_752
output = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
+20
View File
@@ -0,0 +1,20 @@
base_model = "zhipuai/glm-5"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.48
output = 1.9
cache_read = 0.1
cache_write = 0
[limit]
context = 202_752
output = 202_752
[provider]
npm = "@ai-sdk/openai-compatible"
@@ -0,0 +1,28 @@
name = "Kimi K2.5 (Lightning)"
description = "Kimi multimodal agent model for visual understanding, coding, and planning"
family = "kimi-k2"
release_date = "2026-02-06"
last_updated = "2026-02-06"
attachment = true
reasoning = true
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
temperature = false
tool_call = true
structured_output = true
open_weights = true
[interleaved]
field = "reasoning_content"
[cost]
input = 1.00
output = 3.00
cache_read = 0.20
[limit]
context = 131_072
output = 32_768
[modalities]
input = ["text", "image", "video"]
output = ["text"]
@@ -1,13 +1,13 @@
base_model = "alibaba/qwen3.8-27b"
base_model = "moonshotai/kimi-k2.5"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
[cost]
input = 0.25
output = 2.1
cache_read = 0.06
[interleaved]
field = "reasoning_content"
[limit]
output = 262_144
[cost]
input = 0.35
output = 1.7
cache_read = 0.07
[provider]
npm = "@ai-sdk/openai-compatible"
+14
View File
@@ -0,0 +1,14 @@
base_model = "minimax/MiniMax-M2.5"
# Crof's request docs establish no model-specific toggle, effort, or budget.
# https://crof.ai/docs.md (accessed 2026-06-25)
reasoning = true
reasoning_options = []
[cost]
input = 0.11
output = 0.95
cache_read = 0.02
cache_write = 0.375
[provider]
npm = "@ai-sdk/openai-compatible"
@@ -1,14 +0,0 @@
base_model = "deepseek/deepseek-v4-flash-vision-exp"
[[reasoning_options]]
type = "toggle"
[[reasoning_options]]
type = "effort"
values = ["low", "medium", "high", "xhigh", "max"]
[cost]
input = 0.405
output = 1.215
cache_read = 0.0135
cache_write = 0.405
@@ -1,14 +0,0 @@
base_model = "zhipuai/glm-5.3"
[[reasoning_options]]
type = "effort"
values = ["low", "high", "max"]
[cost]
input = 1.2
output = 4.4
cache_read = 0.3
cache_write = 1.2
[limit]
output = 128_000
@@ -1,23 +0,0 @@
# DeepSeek-V4-Flash-Vision-Exp is priced the same as DeepSeek V4 Flash.
# https://api-docs.deepseek.com/quick_start/pricing/ (accessed 2026-08-22)
# https://api-docs.deepseek.com/guides/vision (accessed 2026-08-22)
# OpenAI: `thinking.type = enabled|disabled`, `reasoning_effort = low|high|max`.
# Anthropic: `thinking.type`, `output_config.effort = low|high|max`; budget ignored.
base_model = "deepseek/deepseek-v4-flash-vision-exp"
status = "beta"
[[reasoning_options]]
type = "toggle"
[[reasoning_options]]
type = "effort"
values = ["low", "high", "max"]
[interleaved]
field = "reasoning_content"
[cost]
input = 0.14
output = 0.28
reasoning = 0.28
cache_read = 0.0028
+1 -1
View File
@@ -8,7 +8,7 @@ type = "effort"
values = ["medium", "high", "xhigh"]
[cost]
input = 1.3
input = 0.975
output = 4.3
cache_read = 0.26
+2 -2
View File
@@ -17,8 +17,8 @@ type = "effort"
values = ["none", "low", "medium", "high", "max"]
[cost]
input = 1
output = 3.2
input = 0.75
output = 2.4
cache_read = 0.2
[limit]
+2 -2
View File
@@ -19,8 +19,8 @@ type = "effort"
values = ["none", "low", "medium", "high"]
[cost]
input = 0.5
output = 2.7
input = 0.375
output = 2.025
cache_read = 0.203
[limit]
+2 -2
View File
@@ -19,8 +19,8 @@ type = "effort"
values = ["none", "low", "medium", "high"]
[cost]
input = 0.95
output = 4
input = 0.76
output = 3.2
cache_read = 0.19
[limit]
@@ -15,8 +15,8 @@ type = "effort"
values = ["low", "medium", "high"]
[cost]
input = 0.3
output = 1.2
input = 0.225
output = 0.9
cache_read = 0.06
[limit]
@@ -7,8 +7,8 @@ type = "effort"
values = ["none", "low", "medium", "high", "max"]
[cost]
input = 0.3
output = 0.65
input = 0.165
output = 0.3575
cache_read = 0.06
[limit]
@@ -15,8 +15,8 @@ type = "effort"
values = ["none", "low", "medium", "high"]
[cost]
input = 0.55
output = 3.5
input = 0.3025
output = 1.925
cache_read = 0.111
[limit]
@@ -1,5 +1,6 @@
base_model = "alibaba/qwen3.8-max"
name = "Qwen3.8-Max"
name = "Qwen3.8-2.4T-A95B"
attachment = false
[[reasoning_options]]
type = "effort"
@@ -11,7 +12,8 @@ output = 6
cache_read = 0.2
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text", "image", "video"]
input = ["text"]
@@ -0,0 +1,14 @@
base_model = "alibaba/qwen3.8-27b"
structured_output = false
[[reasoning_options]]
type = "effort"
values = ["none", "low", "medium", "xhigh"]
[cost]
input = 0.45
output = 3.2
cache_read = 0.05
[modalities]
input = ["text", "image"]
@@ -0,0 +1,15 @@
base_model = "alibaba/qwen3.8-27b"
tool_call = false
structured_output = false
[[reasoning_options]]
type = "effort"
values = ["none", "low", "medium", "xhigh"]
[cost]
input = 0.4
output = 3
cache_read = 0.04
[modalities]
input = ["text", "image"]
@@ -1,4 +1,6 @@
base_model = "deepseek/deepseek-v4-pro-0813"
tool_call = false
structured_output = false
[[reasoning_options]]
type = "effort"
@@ -1,5 +1,4 @@
base_model = "tencent/hy3"
name = "Hy3 (NovitaAI)"
structured_output = true
[[reasoning_options]]
@@ -13,4 +12,3 @@ cache_read = 0.035
[limit]
context = 262_144
output = 262_144
@@ -1,13 +0,0 @@
base_model = "deepseek/deepseek-v4-flash-vision-exp"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "high", "max"]
[cost]
input = 0.22
output = 0.66
cache_read = 0.007
[limit]
context = 1_048_576
@@ -8,3 +8,6 @@ values = ["none", "low", "high", "max"]
input = 0.44
output = 1.32
cache_read = 0.014
[limit]
context = 1_048_576
@@ -8,3 +8,6 @@ values = ["none", "high", "max"]
input = 1.32
output = 3.96
cache_read = 0.044
[limit]
context = 1_048_576
@@ -5,9 +5,9 @@ type = "effort"
values = ["none", "low", "high", "max"]
[cost]
input = 0.22
output = 0.66
cache_read = 0.007
input = 0.14
output = 0.28
cache_read = 0.028
[limit]
context = 1_048_576
@@ -4,5 +4,5 @@ tool_call = false
structured_output = false
[cost]
input = 0.760435
output = 0.760435
input = 0.75244
output = 0.75244
@@ -7,5 +7,5 @@ type = "effort"
values = ["low", "medium", "high"]
[cost]
input = 0.175485
output = 0.760435
input = 0.17364
output = 0.75244
@@ -2,5 +2,5 @@ base_model = "mistral/codestral-latest"
structured_output = true
[cost]
input = 0.3
output = 0.9
input = 1
output = 3
@@ -8,7 +8,7 @@ input = 2
output = 5
[limit]
context = 262_144
context = 131_072
[modalities]
input = ["text", "image"]
@@ -6,8 +6,8 @@ type = "effort"
values = ["none", "high"]
[cost]
input = 0.15
output = 0.6
input = 0.06
output = 0.18
[limit]
context = 262_144
@@ -1,13 +0,0 @@
base_model = "deepseek/deepseek-v4-flash-0731"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "high", "max"]
[cost]
input = 0.14
output = 0.28
cache_read = 0.14
[limit]
context = 1_048_576
@@ -9,3 +9,6 @@ values = ["none", "low", "medium"]
input = 0.3
output = 0.9
cache_read = 0.3
[limit]
context = 8_000

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