Compare commits
1 Commits
dev
..
issue-5038
| Author | SHA1 | Date | |
|---|---|---|---|
| e35806b331 |
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
|
||||
@@ -52,7 +52,6 @@ export const ModelFamilyValues = [
|
||||
|
||||
// Meta Muse
|
||||
"muse",
|
||||
"muse-free",
|
||||
|
||||
// Alibaba Qwen
|
||||
"qwen",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" }]'),
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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"] }]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -8,3 +8,6 @@ cache_read = 1.33
|
||||
|
||||
[limit]
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -12,6 +12,3 @@ cache_read = 0.156
|
||||
|
||||
[limit]
|
||||
output = 400_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
|
||||
@@ -12,3 +12,6 @@ cache_read = 0.242
|
||||
|
||||
[limit]
|
||||
output = 1_050_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -16,4 +16,4 @@ cache_read = 0.193
|
||||
output = 256_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -10,4 +10,4 @@ output = 3.5
|
||||
cache_read = 0.201
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -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 AI’s 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"]
|
||||
|
||||
@@ -19,5 +19,5 @@ context = 1_000_000
|
||||
output = 1_000_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -19,5 +19,5 @@ context = 300_000
|
||||
output = 300_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -8,7 +8,7 @@ type = "effort"
|
||||
values = ["medium", "high", "xhigh"]
|
||||
|
||||
[cost]
|
||||
input = 1.3
|
||||
input = 0.975
|
||||
output = 4.3
|
||||
cache_read = 0.26
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -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
Reference in New Issue
Block a user