Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 948aeb6c7b fix: Bedrock: Mantle models template api on ${AWS_REGION}, but aren't served in every region 2026-08-16 10:24:08 +00:00
1281 changed files with 2663 additions and 14928 deletions
@@ -1,24 +0,0 @@
name = "Qwen3 VL 235B A22B Instruct"
description = "Qwen vision-language instruct model for visual reasoning, documents, and agent tasks"
family = "qwen"
release_date = "2025-09-23"
last_updated = "2025-09-23"
attachment = true
reasoning = false
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-03-31"
open_weights = true
[limit]
context = 131_072
output = 32_768
[modalities]
input = ["text", "image"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/Qwen/Qwen3-VL-235B-A22B-Instruct"
@@ -1,24 +0,0 @@
name = "Qwen3 VL 235B A22B Thinking"
description = "Qwen vision-language thinking model for visual reasoning, documents, and agent tasks"
family = "qwen"
release_date = "2025-09-23"
last_updated = "2025-09-23"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-03-31"
open_weights = true
[limit]
context = 131_072
output = 32_768
[modalities]
input = ["text", "image"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/Qwen/Qwen3-VL-235B-A22B-Thinking"
-33
View File
@@ -1,33 +0,0 @@
# Sources (accessed 2026-08-16):
# https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B
# https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B/raw/main/README.md
# https://qwen.ai/blog?id=qwen3.8
# https://openrouter.ai/qwen/qwen3.8-2.4t-a95b
# Open-weight twin of Qwen3.8 Max: text-only, thinking always on,
# reasoning_effort low|medium|xhigh (default xhigh). Native context 262K,
# extensible to ~1.01M. Distinct from closed multimodal qwen3.8-max.
name = "Qwen3.8 2.4T A95B"
description = "Open-weight sparse MoE (2.4T total, 95B active), the open-weight twin of Qwen3.8 Max for coding, research, complex reasoning, and agentic workflows"
family = "qwen"
release_date = "2026-08-12"
last_updated = "2026-08-12"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
license = "qwen3.8-max"
[limit]
context = 262_144
output = 131_072
[modalities]
input = ["text"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B"
@@ -1,19 +0,0 @@
name = "DeepSeek V4 Flash Vision Exp"
description = "Experimental multimodal DeepSeek V4 Flash model for image understanding, coding, and agentic work"
family = "deepseek-flash"
release_date = "2026-08-21"
last_updated = "2026-08-21"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[limit]
context = 1_000_000
output = 384_000
[modalities]
input = ["text", "image"]
output = ["text"]
-22
View File
@@ -1,22 +0,0 @@
# https://ofox.ai/models/deepseek/deepseek-v4-pro-0423
# https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro
name = "DeepSeek V4 Pro 0423"
description = "DeepSeek V4 Pro initial snapshot with million-token context and support for thinking and non-thinking modes"
family = "deepseek-thinking"
release_date = "2026-04-23"
last_updated = "2026-04-23"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-05"
open_weights = true
[limit]
context = 1_000_000
output = 384_000
[modalities]
input = ["text"]
output = ["text"]
+2 -8
View File
@@ -1,16 +1,14 @@
# https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813
name = "DeepSeek V4 Pro 0813"
description = "DeepSeek V4 Pro snapshot with million-token context and support for thinking and non-thinking modes"
family = "deepseek-thinking"
release_date = "2026-08-12"
last_updated = "2026-08-22"
last_updated = "2026-08-12"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
license = "MIT"
open_weights = false
[limit]
context = 1_000_000
@@ -19,7 +17,3 @@ output = 384_000
[modalities]
input = ["text"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813"
+5 -5
View File
@@ -1,15 +1,15 @@
# Tracks the current Gemini Flash release (gemini-3.7-flash).
# Tracks the current Gemini Flash release (gemini-3.5-flash).
name = "Gemini Flash Latest"
description = "High-efficiency Gemini model for agentic workflows, coding, and multimodal reasoning"
description = "Fast Gemini model balancing multimodal reasoning, tool use, and cost"
family = "gemini-flash"
release_date = "2026-08-13"
last_updated = "2026-08-13"
release_date = "2026-05-19"
last_updated = "2026-05-19"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2026-03"
knowledge = "2025-01"
open_weights = false
[limit]
+5 -5
View File
@@ -1,15 +1,15 @@
# Tracks the current Gemini Flash-Lite release (gemini-3.5-flash-lite).
# Tracks the current Gemini Flash-Lite release (gemini-3.1-flash-lite).
name = "Gemini Flash-Lite Latest"
description = "Fast Gemini model balancing multimodal reasoning, tool use, and cost"
description = "Low-latency Gemini model for high-volume multimodal and agent workloads"
family = "gemini-flash-lite"
release_date = "2026-07-21"
last_updated = "2026-07-21"
release_date = "2026-05-07"
last_updated = "2026-05-07"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2026-03"
knowledge = "2025-01"
open_weights = false
[limit]
-24
View File
@@ -1,24 +0,0 @@
# Sources (accessed 2026-08-16):
# https://docs.mistral.ai/models/model-cards/voxtral-small-25-07
# https://mistral.ai/news/voxtral/
# Field values mirror Mistral's own first-party host entry in this repo
# (providers/mistral/models/voxtral-small-latest.toml); host-scoped keys
# (cost, status) are intentionally left to the provider files.
name = "Voxtral Small (latest)"
description = "Instruct model with native audio input for speech understanding and tool use"
family = "voxtral"
release_date = "2025-07-15"
last_updated = "2025-07-15"
attachment = true
reasoning = false
temperature = true
tool_call = true
open_weights = true
[limit]
context = 32_000
output = 32_000
[modalities]
input = ["text", "audio"]
output = ["text"]
@@ -1,25 +0,0 @@
# Sources (accessed 2026-08-16):
# https://docs.perplexity.ai/docs/sonar/models/sonar-deep-research
# https://docs.perplexity.ai/api-reference/sonar-post
# Field values mirror Perplexity's own first-party host entry in this repo
# (providers/perplexity/models/sonar-deep-research.toml); host-scoped keys
# (cost, reasoning_options) are intentionally left to the provider files.
name = "Sonar Deep Research"
description = "Sonar search model for autonomous research and citation-backed long-form reports"
family = "sonar"
release_date = "2025-02-01"
last_updated = "2025-09-01"
attachment = false
reasoning = true
temperature = false
tool_call = false
knowledge = "2025-01"
open_weights = false
[limit]
context = 128_000
output = 32_768
[modalities]
input = ["text"]
output = ["text"]
-59
View File
@@ -1,59 +0,0 @@
name = "Sakana Namazu"
description = "Japanese-specialized reasoning model based on Kimi K2.6 and tuned for Japanese language, culture, and business workflows"
family = "sakana-namazu"
release_date = "2026-08-03"
last_updated = "2026-08-03"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[limit]
context = 262_144
output = 65_536
[modalities]
input = ["text", "image", "pdf"]
output = ["text"]
[[links]]
label = "Official product page"
url = "https://sakana.ai/namazu/"
type = "announcement"
[[links]]
label = "Official model documentation"
url = "https://console.sakana.ai/models?model=sakana-namazu"
type = "docs"
[[benchmarks]]
name = "AIME26"
score = 96.67
source = "https://console.sakana.ai/models?model=sakana-namazu"
[[benchmarks]]
name = "MMLU-Pro"
score = 90.33
source = "https://console.sakana.ai/models?model=sakana-namazu"
[[benchmarks]]
name = "LiveCodeBench v6"
score = 90.33
source = "https://console.sakana.ai/models?model=sakana-namazu"
[[benchmarks]]
name = "JFBench"
score = 37.40
source = "https://console.sakana.ai/models?model=sakana-namazu"
[[benchmarks]]
name = "Translation"
score = 52.20
source = "https://console.sakana.ai/models?model=sakana-namazu"
[[benchmarks]]
name = "FairPoliticsQA"
score = 56.30
source = "https://console.sakana.ai/models?model=sakana-namazu"
-33
View File
@@ -1,33 +0,0 @@
# Sources (accessed 2026-08-16):
# https://huggingface.co/swiss-ai/Apertus-8B-Instruct-2509
# https://arxiv.org/abs/2509.14233
# The model card states "Apertus by default supports a context length up to 65,536 tokens",
# Apache-2.0 licensing, and tool use support. Sibling entry: models/swiss-ai/apertus-70b.toml.
name = "Apertus 8B"
description = "Fully open 8B multilingual LLM supporting 1800+ languages with 65K context. Trained on compliant open data. Apache 2.0, EU AI Act compliant."
release_date = "2025-09-02"
last_updated = "2025-09-02"
knowledge = "2025-09"
attachment = false
reasoning = false
temperature = true
tool_call = true
open_weights = true
license = "Apache-2.0"
[limit]
context = 65_536
output = 8_192
[modalities]
input = ["text"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/swiss-ai/Apertus-8B-Instruct-2509"
[[links]]
label = "Paper"
url = "https://arxiv.org/abs/2509.14233"
type = "paper"
-30
View File
@@ -1,30 +0,0 @@
# Sources:
# https://huggingface.co/Trendyol/Trendyol-LLM-Asure-12B
# https://huggingface.co/api/models/Trendyol/Trendyol-LLM-Asure-12B (createdAt, license, base_model)
# https://huggingface.co/Trendyol/Trendyol-LLM-Asure-12B/raw/main/config.json (max_position_embeddings)
# `reasoning` and `tool_call` are not stated on the model card; both were
# measured against a host serving these weights (llmtr.com, 2026-08-16):
# a request carrying `tools` returns no tool_calls, and no reasoning output
# is produced.
name = "Trendyol Asure 12B"
description = "Turkish-language multimodal instruct model built on Gemma 3 12B for e-commerce text, chat, and image-text tasks"
family = "gemma"
release_date = "2026-02-19"
last_updated = "2026-02-20"
attachment = true
reasoning = false
temperature = true
tool_call = false
open_weights = true
license = "Gemma"
[limit]
context = 131_072
[modalities]
input = ["text", "image"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/Trendyol/Trendyol-LLM-Asure-12B"
-25
View File
@@ -1,25 +0,0 @@
# Sources (accessed 2026-08-16):
# https://developers.upstage.ai/docs/apis/chat
# https://developers.upstage.ai/docs/capabilities/generate/reasoning
# Field values mirror Upstage's own first-party host entry in this repo
# (providers/upstage/models/solar-pro2.toml); host-scoped keys (cost,
# reasoning_options) are intentionally left to the provider files.
name = "Solar Pro 2"
description = "Flagship model for demanding analysis, coding, and production agent workflows"
family = "solar-pro"
release_date = "2025-05-20"
last_updated = "2025-05-20"
attachment = false
reasoning = true
temperature = true
knowledge = "2025-03"
tool_call = true
open_weights = false
[limit]
context = 65_536
output = 8_192
[modalities]
input = ["text"]
output = ["text"]
-25
View File
@@ -1,25 +0,0 @@
# Sources (accessed 2026-08-16):
# https://developers.upstage.ai/docs/apis/chat
# https://developers.upstage.ai/docs/capabilities/generate/reasoning
# Field values mirror Upstage's own first-party host entry in this repo
# (providers/upstage/models/solar-pro3.toml); host-scoped keys (cost,
# reasoning_options) are intentionally left to the provider files.
name = "Solar Pro 3"
description = "Flagship model for demanding analysis, coding, and production agent workflows"
family = "solar-pro"
release_date = "2026-01"
last_updated = "2026-01"
attachment = false
reasoning = true
temperature = true
knowledge = "2025-03"
tool_call = true
open_weights = false
[limit]
context = 131_072
output = 8_192
[modalities]
input = ["text"]
output = ["text"]
-26
View File
@@ -1,26 +0,0 @@
# Sources (accessed 2026-08-16):
# https://developers.upstage.ai/docs/apis/chat
# https://developers.upstage.ai/docs/capabilities/generate/reasoning
# Field values mirror Upstage's own first-party host entry in this repo
# (providers/upstage/models/solar-pro4.toml); host-scoped keys (cost,
# reasoning_options) are intentionally left to the provider files.
name = "Solar Pro 4"
description = "Upstage's flagship model, specialized for agentic use"
family = "solar-pro"
release_date = "2026-08-06"
last_updated = "2026-08-06"
attachment = false
reasoning = true
temperature = true
knowledge = "2026-02"
tool_call = true
structured_output = true
open_weights = false
[limit]
context = 524_288
output = 131_072
[modalities]
input = ["text"]
output = ["text"]
-26
View File
@@ -1,26 +0,0 @@
# Sources:
# - https://docs.x.ai/docs/models
# - https://docs.x.ai/developers/models/grok-imagine-image-2.0
# - https://docs.x.ai/docs/guides/image-generation
# - https://x.ai/news/grok-imagine-image-2
# Pricing: $0.04 per image (not token-based; no [cost] authored)
# Release: 2026-08-07 (GA as Quality Mode; API model id grok-imagine-image-2.0)
name = "Grok Imagine Image 2.0"
description = "Image model for prompt-driven generation, editing, and visual design workflows"
family = "grok"
release_date = "2026-08-07"
last_updated = "2026-08-07"
attachment = true
reasoning = false
temperature = false
tool_call = false
open_weights = false
[limit]
context = 8_000
output = 0
[modalities]
input = ["text", "image"]
output = ["image"]
-25
View File
@@ -1,25 +0,0 @@
# Sources (accessed 2026-08-19):
# - https://z.ai/blog/glm-4.6v
# - https://huggingface.co/zai-org/GLM-4.6V-Flash
name = "GLM-4.6V-Flash"
description = "Lightweight GLM vision model for visual reasoning, documents, and multimodal agents"
family = "glm"
release_date = "2025-12-08"
last_updated = "2025-12-08"
attachment = true
reasoning = true
temperature = true
tool_call = true
open_weights = true
[limit]
context = 128_000
output = 32_768
[modalities]
input = ["text", "image", "video"]
output = ["text"]
[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/zai-org/GLM-4.6V-Flash"
-1
View File
@@ -28,7 +28,6 @@
"huggingface:sync": "bun ./packages/core/script/sync-models.ts huggingface",
"kilo:sync": "bun ./packages/core/script/sync-models.ts kilo",
"llmgateway:sync": "bun ./packages/core/script/sync-models.ts llmgateway",
"llmgateway-providers:sync": "bun ./packages/core/script/sync-models.ts llmgateway-providers",
"requesty:sync": "bun ./packages/core/script/sync-models.ts requesty",
"merge-gateway:sync": "bun ./packages/core/script/sync-models.ts merge-gateway",
"nano-gpt:sync": "bun ./packages/core/script/sync-models.ts nano-gpt",
-4
View File
@@ -52,7 +52,6 @@ export const ModelFamilyValues = [
// Meta Muse
"muse",
"muse-free",
// Alibaba Qwen
"qwen",
@@ -367,9 +366,6 @@ export const ModelFamilyValues = [
// Conductor
"fugu",
// Sakana Namazu
"sakana-namazu",
// V0
"v0",
-4
View File
@@ -5,13 +5,9 @@ export const MAX_CREATED_MODELS = 10;
export const MAX_DELETED_MODELS = 10;
export const MAX_MODEL_CHURN = 15;
const REVIEWED_REASONING_PROVIDERS = new Set([
"crossmodel",
"edenai",
"empiriolabs",
"hyper",
"kilo",
"llmgateway",
"llmgateway-providers",
"merge-gateway",
"nano-gpt",
"openrouter",
+4 -25
View File
@@ -21,7 +21,7 @@ import { hyper } from "./providers/hyper.js";
import { huggingface } from "./providers/huggingface.js";
import { inceptron } from "./providers/inceptron.js";
import { kilo } from "./providers/kilo.js";
import { llmgateway, llmgatewayProviders } from "./providers/llmgateway.js";
import { llmgateway } from "./providers/llmgateway.js";
import { mergeGateway } from "./providers/merge-gateway.js";
import { nanoGpt } from "./providers/nano-gpt.js";
import { openai } from "./providers/openai.js";
@@ -98,17 +98,7 @@ export interface SyncProvider<SourceModel> {
existing(id: string): ExistingModel | undefined;
authored(id: string): ExistingModel | undefined;
},
): {
id: string;
model: SyncedModel;
metadata?: { id: string; model: SyncedMetadata };
/**
* Leading comment block for the written file when it has none of its own
* (e.g. the wire-path header every toggle reasoning control requires). A
* header already present on the existing file always wins.
*/
header?: string;
} | undefined;
): { id: string; model: SyncedModel; metadata?: { id: string; model: SyncedMetadata } } | undefined;
}
export interface SyncResult {
@@ -141,7 +131,6 @@ export const providers: {
inceptron: SyncProvider<any>;
kilo: SyncProvider<any>;
llmgateway: SyncProvider<any>;
"llmgateway-providers": SyncProvider<any>;
"merge-gateway": SyncProvider<any>;
"nano-gpt": SyncProvider<any>;
ofox: SyncProvider<any>;
@@ -173,7 +162,6 @@ export const providers: {
inceptron,
kilo,
llmgateway,
"llmgateway-providers": llmgatewayProviders,
"merge-gateway": mergeGateway,
"nano-gpt": nanoGpt,
ofox,
@@ -198,7 +186,6 @@ export const groups = {
"inceptron",
"kilo",
"llmgateway",
"llmgateway-providers",
"merge-gateway",
"nano-gpt",
"ofox",
@@ -283,16 +270,13 @@ export async function syncProvider<SourceModel>(
: preserveBaseModel(translated.model, existing.get(relativePath)?.authored);
const translatedBase = "base_model" in translatedModel ? translatedModel.base_model : undefined;
let resolvedReasoning: boolean | undefined;
let baseReasoningOptions: unknown;
if (translatedBase !== undefined) {
if (translated.metadata?.id === translatedBase) {
resolvedReasoning = translated.metadata.model.reasoning;
baseReasoningOptions = translated.metadata.model.reasoning_options;
} else {
modelMetadata ??= await readModelMetadata(provider.modelsDir);
const canonicalReasoning = modelMetadata[translatedBase]?.reasoning;
resolvedReasoning = typeof canonicalReasoning === "boolean" ? canonicalReasoning : undefined;
baseReasoningOptions = modelMetadata[translatedBase]?.reasoning_options;
}
} else {
resolvedReasoning = existing.get(relativePath)?.toml.reasoning;
@@ -301,7 +285,6 @@ export async function syncProvider<SourceModel>(
translatedModel,
existing.get(relativePath)?.authored,
resolvedReasoning,
baseReasoningOptions,
);
const withDescription = provider.preserveDescriptions === false
? withReasoningOptions
@@ -317,7 +300,7 @@ export async function syncProvider<SourceModel>(
desired.set(relativePath, {
model: parsed.data,
content: ((existing.get(relativePath)?.header || translated.header) ?? "") + formatToml(parsed.data),
content: (existing.get(relativePath)?.header ?? "") + formatToml(parsed.data),
});
}
@@ -493,7 +476,6 @@ export function preserveReasoningOptions(
model: SyncedModel,
existing: ExistingModel | undefined,
resolvedReasoning: boolean | undefined = existing?.reasoning,
baseReasoningOptions: unknown = undefined,
): SyncedModel {
if ((model.reasoning ?? resolvedReasoning) === false) {
const { reasoning_options: _reasoningOptions, ...withoutReasoningOptions } = model;
@@ -501,10 +483,7 @@ export function preserveReasoningOptions(
}
if (model.reasoning_options !== undefined) return model;
if (existing?.reasoning_options === undefined) {
// When the base model already declares reasoning_options, leave the field
// unset so the factored file inherits them — stamping [] here would
// shadow the base's real controls with "no controls".
return (model.reasoning ?? resolvedReasoning) === true && baseReasoningOptions === undefined
return (model.reasoning ?? resolvedReasoning) === true
? { ...model, reasoning_options: [] }
: model;
}
+4 -28
View File
@@ -1,13 +1,7 @@
import { z } from "zod";
import { describeModel } from "../../describe.js";
import type {
ExistingModel,
SyncProvider,
SyncedBaseModel,
SyncedFullModel,
SyncedModel,
} from "../index.js";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
const API_ENDPOINT = "https://inference.baseten.co/v1/models";
@@ -67,7 +61,6 @@ export const baseten = {
},
translateModel(model, context) {
const existing = context.existing(model.id);
const authored = context.authored(model.id);
const baseModel = existing === undefined
? resolveBasetenBaseModel(model.id)
: existing.base_model;
@@ -79,7 +72,7 @@ export const baseten = {
return {
id: model.id,
model: buildBasetenModel(model, existing, baseModel, authored),
model: buildBasetenModel(model, existing, baseModel),
};
},
} satisfies SyncProvider<BasetenModel>;
@@ -109,7 +102,6 @@ export function buildBasetenModel(
model: BasetenModel,
existing: ExistingModel | undefined,
baseModel = existing === undefined ? resolveBasetenBaseModel(model.id) : existing.base_model,
authored?: ExistingModel,
): SyncedModel {
const features = new Set(model.supported_features);
const samplingParameters = new Set(model.supported_sampling_parameters);
@@ -130,9 +122,7 @@ export function buildBasetenModel(
const limit = {
context: model.context_length,
input: existing?.limit?.input,
// Explicit provider limits are serving overrides and sync pins. Baseten's
// catalog has returned a model's context window as its completion limit.
output: authored?.limit?.output ?? model.max_completion_tokens,
output: model.max_completion_tokens,
};
const values: Partial<SyncedFullModel> = {
name: model.name ?? existing?.name,
@@ -169,21 +159,7 @@ export function buildBasetenModel(
if (limit.context === undefined || limit.output === undefined) {
throw new Error(`Baseten model ${model.id} has incomplete token limits required for sync`);
}
const factored = factorBaseModel(
baseModel,
values,
limit,
existing?.base_model_omit,
) as SyncedBaseModel;
if (authored?.limit?.output === undefined) return factored;
return {
...factored,
limit: {
...factored.limit,
output: authored.limit.output,
},
};
return factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
}
const required = z.object({
+2 -12
View File
@@ -13,16 +13,6 @@ const CANONICAL_BASE_MODEL_EXCEPTIONS = {
const EUR_TO_USD = 1.114;
const CortecsModality = z.enum(["text", "audio", "image", "video", "pdf"]);
type CortecsModality = z.infer<typeof CortecsModality>;
function modalities(values: string[]): CortecsModality[] {
const allowed = new Set<CortecsModality>(CortecsModality.options);
const result = values
.map((value) => value.toLowerCase())
.map((value) => value === "file" ? "pdf" : value)
.filter((value): value is CortecsModality => allowed.has(value as CortecsModality));
return [...new Set<CortecsModality>(result.length > 0 ? result : ["text"])];
}
export const CortecsModel = z.object({
id: z.string().min(1),
@@ -36,8 +26,8 @@ export const CortecsModel = z.object({
cache_write_cost: z.number().nonnegative().optional(),
}).passthrough(),
context_size: z.number().int().positive(),
input_modalities: z.array(z.string()).transform(modalities).default(["text"]),
output_modalities: z.array(z.string()).transform(modalities).default(["text"]),
input_modalities: z.array(CortecsModality).default(["text"]),
output_modalities: z.array(CortecsModality).default(["text"]),
supported_features: z.array(z.string()).default([]),
}).passthrough();
+12 -4
View File
@@ -22,13 +22,11 @@ function baseModelExists(modelID: string): boolean {
// CROSSMODEL_MODELS_URL overrides the endpoint (e.g. a local backend) for testing.
const API_ENDPOINT = process.env.CROSSMODEL_MODELS_URL ?? "https://www.crossmodel.ai/api/models";
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] as const;
const ReasoningCapability = z
.object({
supported: z.boolean().optional(),
toggle: z.boolean().nullish().transform((value) => value ?? undefined),
effort: z.array(z.enum(REASONING_EFFORTS)).nullish().transform((value) => value ?? undefined),
effort: z.array(z.string()).nullish().transform((value) => value ?? undefined),
budget_tokens: z
.object({ min: z.number().optional(), max: z.number().optional() })
.nullish()
@@ -162,6 +160,15 @@ function modalities(values: string[] | undefined, fallback: Modality[]): Modalit
return [...new Set(result.length > 0 ? result : fallback)];
}
// models.dev's reasoning_options effort enum (schema.ts ReasoningEffortValue).
// Guarding against it means an unexpected upstream value is dropped instead of
// silently producing a TOML that fails `validate`.
const REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] as const;
type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
function isReasoningEffort(value: string): value is ReasoningEffort {
return (REASONING_EFFORTS as readonly string[]).includes(value);
}
// Project CrossModel's capabilities.reasoning onto models.dev reasoning_options.
// reasoning absent -> undefined (non-reasoning model; option omitted)
// reasoning === {} -> [] (model reasons, no verified user-selectable control)
@@ -172,7 +179,8 @@ function reasoningOptions(model: CrossModelModel): SyncedModel["reasoning_option
const options: NonNullable<SyncedModel["reasoning_options"]> = [];
if (reasoning.toggle === true) options.push({ type: "toggle" });
if (reasoning.effort !== undefined) {
if (reasoning.effort.length > 0) options.push({ type: "effort", values: reasoning.effort });
const values = reasoning.effort.filter(isReasoningEffort);
if (values.length > 0) options.push({ type: "effort", values });
}
if (reasoning.budget_tokens !== undefined) {
const budget: { type: "budget_tokens"; min?: number; max?: number } = { type: "budget_tokens" };
+45 -486
View File
@@ -1,21 +1,16 @@
import { z } from "zod";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { describeModel } from "../../describe.js";
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
import { ReasoningOption } from "../../schema.js";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js";
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
const API_ENDPOINT = "https://api.llmgateway.io/v1/models";
// LLM Gateway names the originating lab in `family`; most already match the
// canonical prefixes understood by resolveModelMetadataBaseModel, and labs
// outside that shared table (e.g. perplexity) resolve through its exact
// `models/` path match without widening the OpenRouter prefix map for every
// other provider. Alias the few that spell the lab differently. (Mirrors
// huggingface's CANONICAL_ORG_PREFIXES.)
// canonical prefixes understood by resolveCanonicalBaseModel. Alias the few that
// spell the lab differently. (Mirrors huggingface's CANONICAL_ORG_PREFIXES.)
const CANONICAL_FAMILY_ALIASES: Record<string, string> = {
grok: "xai",
mistral: "mistralai",
@@ -35,6 +30,10 @@ const Pricing = z.object({
input_cache_write: z.string().optional(),
});
const LLMGatewayProvider = z.object({
reasoning_efforts: z.array(z.string()).optional(),
}).passthrough();
const ReasoningEffortOrder = new Map([
"none",
"minimal",
@@ -55,22 +54,9 @@ export const LLMGatewayModel = z.object({
input_modalities: z.array(z.string()),
output_modalities: z.array(z.string()),
}),
providers: z.array(
z.object({
providerId: z.string().optional(),
vision: z.boolean().optional(),
tools: z.boolean().optional(),
reasoning: z.boolean().optional(),
reasoning_efforts: z.array(
z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"]),
).optional(),
}).passthrough(),
).optional(),
pricing: Pricing,
// Absent for pseudo-models (custom/auto) and some non-text mappings; text
// models always report it.
context_length: z.number().optional(),
max_output: z.number().optional(),
providers: z.array(LLMGatewayProvider),
context_length: z.number(),
supported_parameters: z.array(z.string()),
structured_outputs: z.boolean().optional(),
}).passthrough();
@@ -81,117 +67,31 @@ export const LLMGatewayResponse = z.object({
export type LLMGatewayModel = z.infer<typeof LLMGatewayModel>;
async function fetchLLMGatewayModels(url: string) {
const headers = process.env.LLMGATEWAY_API_KEY
? { Authorization: `Bearer ${process.env.LLMGATEWAY_API_KEY}` }
: undefined;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`LLM Gateway request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
function textOnly(model: LLMGatewayModel) {
const output = model.architecture.output_modalities;
return output.length === 1 && output[0] === "text";
}
// The DevPass (LLM Gateway) provider: the gateway's aggregated catalog of root
// model IDs, auto-routed across upstream providers.
export const llmgateway = {
id: "llmgateway",
name: "DevPass (LLM Gateway)",
name: "LLM Gateway",
modelsDir: "providers/llmgateway/models",
async fetchModels() {
return fetchLLMGatewayModels(API_ENDPOINT);
const headers = process.env.LLMGATEWAY_API_KEY
? { Authorization: `Bearer ${process.env.LLMGATEWAY_API_KEY}` }
: undefined;
const response = await fetch(API_ENDPOINT, { headers });
if (!response.ok) {
throw new Error(`LLM Gateway request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
const data = LLMGatewayResponse.parse(raw).data.filter(textOnly);
// An empty catalog is an upstream fault; syncing it would delete every
// model file, so fail loudly instead.
if (data.length === 0) {
throw new Error("LLM Gateway returned no text models");
}
return data;
return LLMGatewayResponse.parse(raw).data.filter((model) => {
const output = model.architecture.output_modalities;
return output.length === 1 && output[0] === "text";
});
},
translateModel(model, context) {
const translated = buildLLMGatewayModel(model, context.existing(model.id));
if (translated === undefined) {
return undefined;
}
return { id: model.id, model: translated };
},
sourceID(model) {
return model.id;
},
} satisfies SyncProvider<LLMGatewayModel>;
// Every toggle reasoning control requires a leading wire-path comment, and the
// sync runner only carries over headers that already exist on disk. Files this
// sync writes with a toggle get the gateway-wide default; a hand-written
// header on the existing file always wins.
const TOGGLE_HEADER = `# Toggle: $.reasoning_effort = "none" disables thinking; any other accepted
# value (or omitting the field) leaves it on. The gateway maps it to the
# deployment's thinking switch.
# https://docs.llmgateway.io/features/reasoning
`;
function toggleHeader(model: SyncedModel) {
return model.reasoning_options?.some((option) => option.type === "toggle")
? TOGGLE_HEADER
: undefined;
}
// The LLM Gateway provider: one entry per upstream provider mapping, addressed
// the way the gateway accepts provider-pinned requests (`provider/model-id`).
export const llmgatewayProviders = {
id: "llmgateway-providers",
name: "LLM Gateway",
modelsDir: "providers/llmgateway-providers/models",
async fetchModels() {
return fetchLLMGatewayModels(`${API_ENDPOINT}?mapped=true`);
},
parseModels(raw) {
const data = LLMGatewayResponse.parse(raw).data;
// A deployment without the mapped view ignores the query param and returns
// aggregated root IDs (no provider prefix); syncing those here would wipe
// the provider-pinned catalog, so refuse to proceed. An empty response (or
// one left empty after filtering) would silently do the same via the
// delete-missing pass, so it is equally fatal.
if (data.length === 0 || !data.every((model) => model.id.includes("/"))) {
throw new Error("LLM Gateway mapped view unavailable: response is empty or contains unprefixed model ids");
}
// llmgateway/custom is the BYO-model placeholder and llmgateway/auto the
// auto-router; pinning either to a provider is meaningless in this catalog
// (the aggregated llmgateway provider carries `auto`).
const mapped = data.filter((model) => !model.id.startsWith("llmgateway/") && textOnly(model));
if (mapped.length === 0) {
throw new Error("LLM Gateway mapped view returned no text models");
}
// Every mapped entry is one specific provider deployment whose single
// providers[] mapping drives capabilities and reasoning controls. A kept
// entry with zero or several mappings would make the builder silently fall
// back to noisy supported_parameters / sibling defaults, so fail loudly.
const malformed = mapped.filter((model) => model.providers?.length !== 1);
if (malformed.length > 0) {
throw new Error(
`LLM Gateway mapped view returned entries without exactly one provider mapping: ${
malformed.map((model) => model.id).join(", ")
}`,
);
}
return mapped;
},
translateModel(model, context) {
const translated = buildLLMGatewayMappedModel(model, context.existing(model.id));
if (translated === undefined) {
return undefined;
}
return { id: model.id, model: translated, header: toggleHeader(translated) };
},
sourceID(model) {
return model.id;
return {
id: model.id,
model: buildLLMGatewayModel(model, context.existing(model.id)),
};
},
} satisfies SyncProvider<LLMGatewayModel>;
@@ -225,75 +125,12 @@ function modalities(values: string[], fallback: Modality[]): Modality[] {
return [...new Set(result.length > 0 ? result : fallback)];
}
// Modalities as served by a specific deployment: a mapping without vision must
// not carry image/pdf input, regardless of what the model-level architecture
// claims — attachment=false with image input is contradictory.
function deploymentModalities(model: LLMGatewayModel, vision: boolean | undefined) {
const base = defaultModalities(model);
if (vision !== false) {
return base;
}
const input = base.input.filter((value) => value !== "image" && value !== "pdf");
return {
input: input.length > 0 ? input : (["text"] satisfies Modality[]),
output: base.output,
};
}
const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models");
const AGGREGATED_MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers", "llmgateway", "models");
const canonicalOutputLimitByID = new Map<string, number | undefined>();
interface SiblingCuration {
reasoning_options?: SyncedFullModel["reasoning_options"];
interleaved?: SyncedFullModel["interleaved"];
cost_tiers?: NonNullable<SyncedFullModel["cost"]>["tiers"];
}
const siblingCurationByID = new Map<string, SiblingCuration>();
// The aggregated llmgateway catalog curates reasoning controls, the reasoning
// side-channel, and context pricing tiers for the same gateway surface; mapped
// deployments of the same root model reuse them when the deployment does not
// declare its own.
function siblingCuration(rootID: string): SiblingCuration {
let curation = siblingCurationByID.get(rootID);
if (curation === undefined) {
const filePath = path.join(AGGREGATED_MODELS_DIR, `${rootID}.toml`);
const authored = existsSync(filePath)
? Bun.TOML.parse(readFileSync(filePath, "utf8")) as SiblingCuration & {
cost?: { tiers?: NonNullable<SyncedFullModel["cost"]>["tiers"] };
}
: undefined;
curation = {
reasoning_options: authored?.reasoning_options?.length ? authored.reasoning_options : undefined,
interleaved: authored?.interleaved,
cost_tiers: authored?.cost?.tiers,
};
siblingCurationByID.set(rootID, curation);
}
return curation;
}
// Whether the canonical metadata declares limit.output; factored entries can
// only omit their own output override when the base has one to inherit.
function canonicalOutputLimit(modelID: string) {
if (!canonicalOutputLimitByID.has(modelID)) {
const filePath = path.join(MODELS_DIR, `${modelID}.toml`);
const metadata = existsSync(filePath)
? Bun.TOML.parse(readFileSync(filePath, "utf8")) as { limit?: { output?: number } }
: undefined;
canonicalOutputLimitByID.set(modelID, metadata?.limit?.output);
}
return canonicalOutputLimitByID.get(modelID);
}
function resolveLLMGatewayBaseModel(model: LLMGatewayModel, modelID = model.id) {
const alias = BASE_MODEL_ALIASES[modelID];
function resolveLLMGatewayBaseModel(model: LLMGatewayModel) {
const alias = BASE_MODEL_ALIASES[model.id];
if (alias !== undefined) return alias;
if (model.family === undefined) return undefined;
const prefix = CANONICAL_FAMILY_ALIASES[model.family] ?? model.family;
return resolveModelMetadataBaseModel(`${prefix}/${modelID}`);
return resolveCanonicalBaseModel(`${prefix}/${model.id}`);
}
function inferFamily(model: LLMGatewayModel, name: string) {
@@ -315,19 +152,15 @@ function inferFamily(model: LLMGatewayModel, name: string) {
export function buildLLMGatewayModel(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedModel | undefined {
): SyncedModel {
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
const reasoning = model.supported_parameters.includes("reasoning")
|| model.supported_parameters.includes("include_reasoning");
const reasoningOptions = llmGatewayReasoningOptions(model, existing);
const reported = model.context_length ?? 0;
// A missing/zero context must never be authored as limit.context = 0:
// factored entries leave it unset and inherit the base, and unfactored
// creates are skipped entirely. An authored 0 on the existing file is
// equally unusable and must not be re-stamped.
const servedContext = reported > 0 ? reported : undefined;
const context = servedContext ?? (existing?.limit?.context || undefined);
const context = model.context_length > 0
? model.context_length
: existing?.limit?.context ?? model.context_length;
// The gateway is authoritative for the volatile, gateway-specific data — cost,
// served limits, and explicitly advertised reasoning efforts. Its
@@ -346,24 +179,15 @@ export function buildLLMGatewayModel(
tiers: existing?.cost?.tiers,
}
: existing?.cost;
// Authored limits carry only known-positive values — never the zero/absent
// `reported` fallback.
const limit = context !== undefined
? {
context,
input: existing?.limit?.input,
output: (existing?.limit?.output || undefined) ?? context,
}
: undefined;
const limit = {
context,
input: existing?.limit?.input,
output: existing?.limit?.output ?? context,
};
// Existing factored model: refresh cost + limit, keep every authored override
// as-is (undefined fields keep inheriting the base model).
if (existing?.base_model !== undefined) {
const factoredLimit = {
context,
input: existing.limit?.input,
output: existing.limit?.output ?? context,
};
return factorBaseModel(
existing.base_model,
{
@@ -376,7 +200,7 @@ export function buildLLMGatewayModel(
tool_call: existing.tool_call,
structured_output: existing.structured_output,
open_weights: existing.open_weights,
limit: factoredLimit,
limit,
modalities: existing.modalities,
}),
reasoning: existing.reasoning,
@@ -388,22 +212,16 @@ export function buildLLMGatewayModel(
interleaved: existing.interleaved,
knowledge: existing.knowledge,
modalities: existing.modalities,
limit: factoredLimit,
limit,
cost,
},
factoredLimit,
limit,
existing.base_model_omit,
);
}
// Existing full model: refresh cost + limit, preserve curated metadata.
if (existing !== undefined) {
// With no usable context from the API or the file there is nothing valid
// to author, and skipping would hand the file to the delete-missing pass —
// fail loudly rather than write limit.context = 0.
if (limit === undefined) {
throw new Error(`LLM Gateway entry ${model.id} has no usable context to author`);
}
return {
name: existing.name ?? model.name,
description: existing.description ?? describeModel({
@@ -453,12 +271,7 @@ export function buildLLMGatewayModel(
}
// Brand-new model: best-effort translation from the gateway. Capability and
// modality data are unreliable here and should be hand-reviewed. Without a
// positive served context there is nothing usable to author, so skip.
if (servedContext === undefined) {
return undefined;
}
const createdLimit = limit ?? { context: servedContext, input: undefined, output: servedContext };
// modality data are unreliable here and should be hand-reviewed.
const { input, output } = defaultModalities(model);
return {
name: model.name,
@@ -471,7 +284,7 @@ export function buildLLMGatewayModel(
|| model.supported_parameters.includes("tool_choice"),
structured_output: model.structured_outputs ?? false,
open_weights: false,
limit: createdLimit,
limit,
modalities: { input, output },
}),
family: inferFamily(model, model.name),
@@ -486,261 +299,7 @@ export function buildLLMGatewayModel(
structured_output: model.structured_outputs ?? false,
open_weights: false,
cost,
limit: createdLimit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
export function buildLLMGatewayMappedModel(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedModel | undefined {
// Mapped entries carry exactly one provider mapping; its capability flags
// describe that specific deployment, unlike the aggregated view where
// supported_parameters are too noisy to trust.
const mapping = model.providers?.[0];
const rootID = model.id.split("/").slice(1).join("/");
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
// The mapping's flag stays authoritative on resyncs too, so the written
// reasoning boolean and the reasoning_options derived from it always move
// together; prior curation only fills in when the mapping is silent, then
// the noisy supported_parameters signal as a last resort.
const reasoning = mapping?.reasoning
?? existing?.reasoning
?? (model.supported_parameters.includes("reasoning")
|| model.supported_parameters.includes("include_reasoning"));
// The exact reasoning_effort values this deployment accepts. A deployment
// whose only accepted effort is "none" exposes a plain on/off switch (the
// gateway honours it through the thinking toggle), not effort tiers.
const deploymentOptions = mapping?.reasoning_efforts?.length
? mapping.reasoning_efforts.length === 1 && mapping.reasoning_efforts[0] === "none"
? [{ type: "toggle" as const }]
: [{ type: "effort" as const, values: mapping.reasoning_efforts }]
: undefined;
// Deployment-declared efforts own the effort/toggle surface; curation falls
// back from non-empty options on this file to the aggregated llmgateway
// catalog's controls for the same root model on the same gateway surface.
// Curated non-effort controls (e.g. budget_tokens for $.reasoning.max_tokens,
// which this host serves regardless of the effort list) survive alongside
// deployment efforts instead of being wiped by them. A curated [] counts as
// unknown so a bad first stamp is not sticky. Non-reasoning deployments
// carry none; the same applies to the interleaved reasoning side-channel.
const sibling = siblingCuration(rootID);
const curatedOptions = (existing?.reasoning_options?.length ? existing.reasoning_options : undefined)
?? sibling.reasoning_options;
const reasoningOptions = reasoning
? deploymentOptions !== undefined
? [
...(curatedOptions ?? []).filter((option) => option.type !== "effort" && option.type !== "toggle"),
...deploymentOptions,
]
: curatedOptions
: undefined;
const interleaved = reasoning
? existing?.interleaved ?? sibling.interleaved
: undefined;
const reported = model.context_length ?? 0;
// Same zero-context rule as the aggregated builder: never author 0, inherit
// on factored entries, skip unfactored creates. An authored 0 on the
// existing file is equally unusable.
const servedContext = reported > 0 ? reported : undefined;
const context = servedContext ?? (existing?.limit?.context || undefined);
const cost = prompt !== undefined && completion !== undefined
? {
input: prompt,
output: completion,
reasoning: reasoning ? nonZeroPrice(model.pricing.internal_reasoning) ?? existing?.cost?.reasoning : existing?.cost?.reasoning,
cache_read: nonZeroPrice(model.pricing.input_cache_read) ?? existing?.cost?.cache_read,
cache_write: nonZeroPrice(model.pricing.input_cache_write) ?? existing?.cost?.cache_write,
// The gateway API does not expose context pricing tiers, so authored
// tiers stick and new files seed from the aggregated sibling's curated
// tiers rather than silently under-stating long-context pricing.
tiers: existing?.cost?.tiers ?? sibling.cost_tiers,
}
: existing?.cost;
// The gateway's max_output is the deployment's real served limit, so it wins
// over inherited/authored values, unlike the aggregated view.
const servedOutput = (model.max_output || undefined) ?? (existing?.limit?.output || undefined);
// Authored limits carry only known-positive values — never the zero/absent
// `reported` fallback.
const limit = context !== undefined
? {
context,
input: existing?.limit?.input,
output: servedOutput ?? context,
}
: undefined;
// Existing factored model: refresh cost + limit, keep every authored override
// as-is. Unlike the aggregated provider, the name override must be carried
// forward: mapped names disambiguate deployments of the same model (e.g.
// "GPT-5.5 (Azure)" vs "GPT-5.5 (OpenAI)") and must not collapse back to the
// base metadata name.
if (existing?.base_model !== undefined) {
// Mirror the brand-new factored path: without a served or authored output,
// keep inheriting the base's output rather than stamping context over it.
const factoredLimit = {
context,
input: existing.limit?.input,
output: servedOutput ?? (canonicalOutputLimit(existing.base_model) !== undefined ? undefined : context),
};
// Deployment capability flags keep their create-path authority on
// resyncs: a mapping that gains or loses reasoning/vision/tools/structured
// outputs realigns the written flags together with the reasoning_options
// computed from them, instead of freezing stale curation forever.
return factorBaseModel(
existing.base_model,
{
name: existing.name ?? model.name,
attachment: mapping?.vision ?? existing.attachment,
// No describeModel fallback: synthesizing a description here would
// stamp a sticky generic override on every name-pinned factored entry;
// leaving it unset keeps inheriting the lab text from the base.
description: existing.description,
reasoning: mapping?.reasoning ?? existing.reasoning,
reasoning_options: reasoningOptions,
temperature: existing.temperature,
tool_call: mapping?.tools ?? existing.tool_call,
structured_output: model.structured_outputs ?? existing.structured_output,
status: existing.status,
interleaved,
knowledge: existing.knowledge,
// Vision realigns modalities in both directions: false strips
// image/pdf, true clears any stale stripped override so the base's
// richer inputs inherit again; only a silent mapping keeps curation.
modalities: mapping?.vision === undefined
? existing.modalities
: mapping.vision
? undefined
: deploymentModalities(model, false),
limit: factoredLimit,
cost,
},
factoredLimit,
existing.base_model_omit,
);
}
// Existing full model: refresh cost + limit, preserve curated metadata.
// Capability flags follow the same rule as the factored path above: the
// deployment mapping wins, curation fills the gaps.
if (existing !== undefined) {
// With no usable context from the API or the file there is nothing valid
// to author, and skipping would hand the file to the delete-missing pass —
// fail loudly rather than write limit.context = 0.
if (limit === undefined) {
throw new Error(`LLM Gateway mapped entry ${model.id} has no usable context to author`);
}
const resolved = {
attachment: mapping?.vision ?? existing.attachment ?? false,
tool_call: mapping?.tools ?? existing.tool_call ?? false,
structured_output: model.structured_outputs ?? existing.structured_output,
// Same bidirectional vision rule as the factored path; with no base to
// inherit from, a declared vision recomputes from the served
// architecture instead of clearing.
modalities: mapping?.vision === undefined
? existing.modalities ?? deploymentModalities(model, undefined)
: deploymentModalities(model, mapping.vision),
};
return {
name: existing.name ?? model.name,
description: existing.description ?? describeModel({
id: model.id,
name: existing.name ?? model.name,
family: existing.family,
reasoning,
tool_call: resolved.tool_call,
structured_output: resolved.structured_output,
open_weights: existing.open_weights,
limit,
modalities: resolved.modalities,
}),
family: existing.family,
release_date: existing.release_date ?? dateFromTimestamp(model.created),
last_updated: existing.last_updated ?? dateFromTimestamp(model.created),
attachment: resolved.attachment,
reasoning,
reasoning_options: reasoningOptions,
temperature: existing.temperature ?? false,
tool_call: resolved.tool_call,
structured_output: resolved.structured_output,
knowledge: existing.knowledge,
open_weights: existing.open_weights ?? false,
status: existing.status,
interleaved,
cost,
limit,
modalities: resolved.modalities,
} satisfies SyncedFullModel;
}
// Brand-new model with a reviewed metadata entry: factor against the
// canonical base. The mapped ID is `serving-provider/model-id` and the
// serving provider is unrelated to the originating lab, so resolve the base
// from the root model ID + family, and keep the disambiguating name. The
// mapping's own capability flags describe this specific deployment, so they
// go in as overrides (factorBaseModel drops the ones equal to the base).
const canonical = resolveLLMGatewayBaseModel(model, rootID);
if (canonical !== undefined) {
const factoredLimit = {
context,
input: undefined,
// Without a served limit, inherit the base's output; only fall back to
// context when the base declares none (output is required downstream).
output: model.max_output ?? (canonicalOutputLimit(canonical) !== undefined ? undefined : context),
};
return factorBaseModel(canonical, {
name: model.name,
attachment: mapping?.vision,
reasoning: mapping?.reasoning,
reasoning_options: reasoningOptions,
interleaved,
tool_call: mapping?.tools,
structured_output: model.structured_outputs,
// A deployment without vision must not inherit image/pdf inputs from
// the base — attachment=false with image input is contradictory.
modalities: mapping?.vision === false ? deploymentModalities(model, false) : undefined,
limit: factoredLimit,
cost,
}, factoredLimit);
}
// Brand-new model without metadata: best-effort translation. The mapping's
// own capability flags are reliable here; modalities mirror the mapping too.
// Without a positive served context there is nothing usable to author.
if (servedContext === undefined) {
return undefined;
}
const createdLimit = limit ?? { context: servedContext, input: undefined, output: servedOutput ?? servedContext };
const { input, output } = deploymentModalities(model, mapping?.vision);
return {
name: model.name,
description: describeModel({
id: model.id,
name: model.name,
family: inferFamily(model, model.name),
reasoning,
tool_call: mapping?.tools ?? false,
structured_output: model.structured_outputs ?? false,
open_weights: false,
limit: createdLimit,
modalities: { input, output },
}),
family: inferFamily(model, model.name),
release_date: dateFromTimestamp(model.created),
last_updated: dateFromTimestamp(model.created),
attachment: mapping?.vision ?? input.some((value) => value !== "text"),
reasoning,
reasoning_options: reasoningOptions,
interleaved,
temperature: model.supported_parameters.includes("temperature"),
tool_call: mapping?.tools ?? false,
structured_output: model.structured_outputs ?? false,
open_weights: false,
cost,
limit: createdLimit,
limit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
@@ -749,7 +308,7 @@ function llmGatewayReasoningOptions(
model: LLMGatewayModel,
existing: ExistingModel | undefined,
): SyncedFullModel["reasoning_options"] {
const advertised = new Set((model.providers ?? []).flatMap((provider) => provider.reasoning_efforts ?? []));
const advertised = new Set(model.providers.flatMap((provider) => provider.reasoning_efforts ?? []));
if (advertised.size === 0) return undefined;
const efforts = [...advertised].sort((a, b) => {
@@ -68,7 +68,7 @@ export const OpenRouterModel = z.object({
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
overrides: z.array(z.object({
min_prompt_tokens: z.number().optional(),
min_prompt_tokens: z.number(),
prompt: z.string().optional(),
completion: z.string().optional(),
input_cache_read: z.string().optional(),
@@ -159,7 +159,7 @@ function costTiers(model: OpenRouterModel, existing: ExistingModel | undefined)
.flatMap((o) => {
const input = price(o.prompt);
const output = price(o.completion);
if (o.min_prompt_tokens === undefined || input === undefined || output === undefined) return [];
if (input === undefined || output === undefined) return [];
return [{
tier: { type: "context" as const, size: o.min_prompt_tokens },
input,
+6 -42
View File
@@ -69,17 +69,9 @@ export const vercel = {
return VercelResponse.parse(raw).data;
},
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);
return {
id: model.id,
model: buildVercelModel(
model,
existing,
inherited ?? (baseModel === undefined || baseModel === model.id ? undefined : context.existing(baseModel)),
),
model: buildVercelModel(model, context.existing(model.id)),
};
},
sameModel(current, desired) {
@@ -87,11 +79,7 @@ export const vercel = {
},
} satisfies SyncProvider<VercelModel>;
export function buildVercelModel(
model: VercelModel,
existing: ExistingModel | undefined,
base: ExistingModel | undefined = undefined,
): SyncedModel {
export function buildVercelModel(model: VercelModel, existing: ExistingModel | undefined): SyncedModel {
const tags = new Set(model.tags);
const releaseDate = model.released
? dateFromTimestamp(model.released)
@@ -145,7 +133,7 @@ export function buildVercelModel(
last_updated: existing?.last_updated ?? releaseDate,
attachment: existing?.attachment ?? (tags.has("vision") || tags.has("file-input")),
reasoning: existing?.reasoning ?? tags.has("reasoning"),
reasoning_options: existing?.reasoning_options ?? base?.reasoning_options,
reasoning_options: existing?.reasoning_options,
temperature: existing?.temperature,
tool_call: model.type === "language"
? existing?.tool_call ?? tags.has("tool-use")
@@ -180,35 +168,11 @@ 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, {
name: synced.name,
attachment: synced.attachment,
reasoning: synced.reasoning,
reasoning_options: synced.reasoning_options,
temperature: synced.temperature,
tool_call: synced.tool_call,
structured_output: synced.structured_output,
status: synced.status,
interleaved: synced.interleaved,
experimental: synced.experimental,
provider: synced.provider,
cost: synced.cost,
limit: synced.limit,
modalities: synced.modalities,
}, 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;
const { last_updated: _lastUpdated, ...overrides } = synced;
return factorBaseModel(baseModel, overrides, synced.limit, existing?.base_model_omit);
}
function dateFromTimestamp(timestamp: number) {
+1 -1
View File
@@ -82,7 +82,7 @@ test("does not inspect deleted models", async () => {
});
test("allows reviewed providers with explicit reasoning options", async () => {
for (const provider of ["crossmodel", "edenai", "empiriolabs", "hyper", "kilo", "llmgateway", "llmgateway-providers", "merge-gateway", "nano-gpt", "openrouter", "venice"]) {
for (const provider of ["empiriolabs", "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" }]'),
-48
View File
@@ -1,48 +0,0 @@
import { expect, test } from "bun:test";
import {
buildBasetenModel,
type BasetenModel,
} from "../src/sync/providers/baseten.js";
function basetenModel(overrides: Partial<BasetenModel> = {}): BasetenModel {
return {
id: "deepseek-ai/DeepSeek-V4-Flash-0731",
name: "DeepSeek V4 Flash 0731",
context_length: 1_048_576,
max_completion_tokens: 1_048_576,
input_modalities: ["text"],
output_modalities: ["text"],
pricing: { prompt: "0.00000013", completion: "0.00000026" },
supported_features: ["reasoning", "tools", "structured_outputs"],
supported_sampling_parameters: ["temperature"],
...overrides,
};
}
test("preserves an explicitly authored Baseten output limit", () => {
const built = buildBasetenModel(
basetenModel(),
undefined,
"deepseek/deepseek-v4-flash-0731",
{ limit: { context: 1_048_576, output: 384_000 } },
);
expect(built).toMatchObject({
base_model: "deepseek/deepseek-v4-flash-0731",
limit: { context: 1_048_576, output: 384_000 },
});
});
test("uses Baseten's catalog output limit without an authored override", () => {
const built = buildBasetenModel(
basetenModel({ max_completion_tokens: 262_144 }),
undefined,
"deepseek/deepseek-v4-pro-0813",
);
expect(built).toMatchObject({
base_model: "deepseek/deepseek-v4-pro-0813",
limit: { context: 1_048_576, output: 262_144 },
});
});
+3 -545
View File
@@ -10,7 +10,7 @@ import {
parseAnthropicPricing,
type AnthropicModel,
} from "../src/sync/providers/anthropic.js";
import { buildCortecsModel, cortecs, type CortecsModel } from "../src/sync/providers/cortecs.js";
import { buildCortecsModel, type CortecsModel } from "../src/sync/providers/cortecs.js";
import {
buildCrossModel,
CrossModelResponse,
@@ -56,13 +56,7 @@ import {
resolveCanonicalBaseModel,
type OpenRouterModel,
} from "../src/sync/providers/openrouter.js";
import {
buildLLMGatewayMappedModel,
buildLLMGatewayModel,
llmgateway,
llmgatewayProviders,
type LLMGatewayModel,
} from "../src/sync/providers/llmgateway.js";
import { buildLLMGatewayModel, type LLMGatewayModel } from "../src/sync/providers/llmgateway.js";
import {
buildMergeGatewayModel,
fetchMergeGatewayModels,
@@ -473,46 +467,6 @@ test("parses CrossModel's nullable reasoning controls", () => {
});
});
test("syncs CrossModel's explicit reasoning controls", () => {
const model = buildCrossModel(
crossModelModel({
capabilities: {
json: true,
reasoning: {
supported: true,
toggle: true,
effort: ["low", "high", "max"],
budget_tokens: { min: 1_024, max: 32_000 },
},
},
}),
undefined,
);
expect(model).toMatchObject({
reasoning_options: [
{ type: "toggle" },
{ type: "effort", values: ["low", "high", "max"] },
{ type: "budget_tokens", min: 1_024, max: 32_000 },
],
});
});
test("rejects unknown CrossModel reasoning efforts", () => {
expect(() =>
CrossModelResponse.parse({
data: [
{
...crossModelModel(),
capabilities: {
reasoning: { supported: true, effort: ["unexpected"] },
},
},
],
})
).toThrow();
});
test("syncs NanoGPT's verified reasoning, pricing, limits, and open-weight metadata", () => {
const model = buildNanoGptModel(nanoGptModel({
pricing: {
@@ -2080,33 +2034,6 @@ test("OpenRouter sync maps pricing.overrides into cost tiers", () => {
});
});
test("OpenRouter sync ignores time-window pricing overrides", () => {
const source = openRouterModel({
pricing: {
prompt: "0.00000132",
completion: "0.00000396",
overrides: [{
utc_start: 1_000,
utc_end: 100,
prompt: "0.00000066",
completion: "0.00000198",
}],
},
});
const [parsed] = openrouter.parseModels({ data: [source] });
const model = buildOpenRouterModel(parsed!, {
cost: {
input: 1.32,
output: 3.96,
tiers: [{ tier: { type: "context", size: 200_000 }, input: 2.64, output: 7.92 }],
},
});
expect(model.cost?.tiers).toEqual([
{ tier: { type: "context", size: 200_000 }, input: 2.64, output: 7.92 },
]);
});
test("OpenRouter sync keeps authored tiers when API omits overrides", () => {
const model = buildOpenRouterModel(openRouterModel({
pricing: {
@@ -2681,27 +2608,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 +2938,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 +2961,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 +3514,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: [{
@@ -4044,26 +3533,11 @@ test("Vercel Claude Opus fast variants factor onto base opus metadata", () => {
}],
});
const translated = vercel.translateModel(model!, {
existing(id) {
return id === "anthropic/claude-opus-5"
? { reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }] }
: undefined;
},
authored() {
return undefined;
},
});
const synced = translated?.model;
expect(synced).toMatchObject({
expect(buildVercelModel(model!, undefined)).toMatchObject({
base_model: "anthropic/claude-opus-5",
name: "Claude Opus 5 (Fast)",
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
cost: { input: 10, output: 50, cache_read: 1, cache_write: 12.5 },
});
expect(synced).not.toHaveProperty("description");
expect(synced).not.toHaveProperty("family");
});
test("OpenRouter Claude Opus fast variants factor onto base opus metadata", () => {
@@ -4303,22 +3777,6 @@ function llmGatewayModel(overrides: Partial<LLMGatewayModel> = {}): LLMGatewayMo
};
}
function llmGatewayMappedModel(overrides: Partial<LLMGatewayModel> = {}): LLMGatewayModel {
return llmGatewayModel({
id: "anthropic/claude-fable-5",
name: "Claude Fable 5 (Anthropic)",
providers: [{
providerId: "anthropic",
vision: true,
tools: true,
reasoning: true,
reasoning_efforts: ["low", "medium", "high", "xhigh", "max"],
}],
max_output: 128_000,
...overrides,
});
}
function mergeGatewayVendor(
overrides: Partial<MergeGatewayModel["vendors"][string]> = {},
): MergeGatewayModel["vendors"][string] {
+1 -4
View File
@@ -68,10 +68,7 @@ export interface CostTier extends Cost {
/** Pricing for a provider's model, including context-size tiers. */
export interface ModelCost extends Cost {
/**
* Legacy compatibility field for context-tier pricing.
* @deprecated Use `tiers` to read the exact context threshold.
*/
/** Legacy compatibility field: pricing applied beyond 200K context. Prefer `tiers`. */
context_over_200k?: Cost
/** Context-size-based pricing tiers. */
tiers?: CostTier[]
-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" role="img">
<title>AgentRouter</title>
<path d="M11.8 5C7.8 4.2 3 9.6 4.2 15.6c.4 3 3 4.6 6.6 3-1.8-3-2.6-7.8 1-13.6Zm.4 14c4 .8 8.8-4.6 7.6-10.6-.4-3-3-4.6-6.6-3 1.8 3 2.6 7.8-1 13.6ZM12 9.5c.2 1.5 1 2.3 2.5 2.5-1.5.2-2.3 1-2.5 2.5-.2-1.5-1-2.3-2.5-2.5 1.5-.2 2.3-1 2.5-2.5Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 370 B

@@ -1,8 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "anthropic/claude-opus-4-8"
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[provider]
npm = "@ai-sdk/anthropic"
api = "https://agentrouter.org/v1"
@@ -1,8 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "anthropic/claude-opus-5"
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[provider]
npm = "@ai-sdk/anthropic"
api = "https://agentrouter.org/v1"
@@ -1,4 +0,0 @@
# Catalog and endpoint support: https://agentrouter.org/api/pricing
# AgentRouter publishes relative token ratios, not USD prices, so cost is intentionally omitted.
base_model = "openai/gpt-5.6-sol"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
-5
View File
@@ -1,5 +0,0 @@
name = "AgentRouter"
env = ["AGENTROUTER_API_KEY"]
npm = "@ai-sdk/openai-compatible"
api = "https://agentrouter.org/v1"
doc = "https://agentrouter.org/docs/opencode.html"
@@ -1,13 +0,0 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
base_model = "deepseek/deepseek-v4-flash-0731"
reasoning_options = [{ type = "effort", values = ["none", "low", "high", "max"] }]
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.20
output = 0.50
cache_read = 0.10
[limit]
context = 1_048_576
output = 81_920
+1
View File
@@ -2,6 +2,7 @@
# AKI.IO uses the reasoning_option 'none' to disable reasoning
base_model = "google/gemma-4-26b-a4b-it"
attachment = false
reasoning = true
reasoning_options = [{ type = "effort", values = ["none", "high"] }]
[cost]
@@ -4,11 +4,9 @@ base_model = "moonshotai/kimi-k2.7-code"
attachment = false
reasoning_options = []
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.86
output = 3.00
cache_read = 0.18
[limit]
context = 262_144
@@ -0,0 +1,12 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
base_model = "minimax/MiniMax-M2.5"
attachment = false
reasoning_options = []
[cost]
input = 0.25
output = 1.20
[limit]
context = 196_608
output = 65_536
-14
View File
@@ -1,14 +0,0 @@
# attachment are not supported on AKI.IO for security reasons and therefore are disabled
# AKI.IO uses the reasoning_option 'none' to disable reasoning
base_model = "alibaba/qwen3.8-27b"
attachment = false
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "xhigh"] }]
# cost as described on the AKI.IO pricing page at https://aki.io/#pricing
[cost]
input = 0.30
output = 2.20
cache_read = 0.10
[modalities]
input = ["text", "image"]
@@ -21,8 +21,6 @@ max = 81_920
input = 0.115
output = 0.287
reasoning = 1.147
cache_read = 0.012
cache_write = 0.144
[limit]
context = 1_000_000
@@ -1,20 +1,19 @@
# Global cross-Region inference profile — served by Bedrock core (Converse), not Mantle, so no [provider] override.
# Verified from eu-west-1: returns 200 with reasoning effort set.
# Pricing mirrors the US in-region on-demand rates on openai.gpt-5.6-luna: https://aws.amazon.com/bedrock/pricing/
# bedrock-runtime global CRIS ID (not Mantle). Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html
# Global CRIS pricing (USD per 1M tokens): short ≤272K / long >272K — input / 30m cache write / cache read / output
base_model = "openai/gpt-5.6-luna"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
base_model_omit = ["cost.context_over_200k", "experimental.modes.fast"]
name = "GPT-5.6 Luna (Global)"
[cost]
input = 0.22
output = 1.32
cache_read = 0.022
cache_write = 0.275
input = 0.20
output = 1.20
cache_read = 0.02
cache_write = 0.25
[[cost.tiers]]
tier = { size = 272_000 }
input = 0.44
output = 1.98
cache_read = 0.044
cache_write = 0.55
input = 0.40
output = 1.80
cache_read = 0.04
cache_write = 0.50
@@ -1,20 +1,19 @@
# Global cross-Region inference profile — served by Bedrock core (Converse), not Mantle, so no [provider] override.
# Verified from eu-west-1: returns 200 with reasoning effort set.
# Pricing mirrors the US in-region on-demand rates on openai.gpt-5.6-sol: https://aws.amazon.com/bedrock/pricing/
# bedrock-runtime global CRIS ID (not Mantle). Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html
# Global CRIS pricing (USD per 1M tokens): short ≤272K / long >272K — input / 30m cache write / cache read / output
base_model = "openai/gpt-5.6-sol"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
base_model_omit = ["cost.context_over_200k", "experimental.modes.fast"]
name = "GPT-5.6 Sol (Global)"
[cost]
input = 5.50
output = 33.00
cache_read = 0.55
cache_write = 6.875
input = 5.00
output = 30.00
cache_read = 0.50
cache_write = 6.25
[[cost.tiers]]
tier = { size = 272_000 }
input = 11.00
output = 49.50
cache_read = 1.10
cache_write = 13.75
input = 10.00
output = 45.00
cache_read = 1.00
cache_write = 12.50
@@ -1,20 +1,19 @@
# Global cross-Region inference profile — served by Bedrock core (Converse), not Mantle, so no [provider] override.
# Verified from eu-west-1: effort none|low|medium|high|xhigh|max all return 200.
# Pricing mirrors the US in-region on-demand rates on openai.gpt-5.6-terra: https://aws.amazon.com/bedrock/pricing/
# bedrock-runtime global CRIS ID (not Mantle). Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html
# Global CRIS pricing (USD per 1M tokens): short ≤272K / long >272K — input / 30m cache write / cache read / output
base_model = "openai/gpt-5.6-terra"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
base_model_omit = ["cost.context_over_200k", "experimental.modes.fast"]
name = "GPT-5.6 Terra (Global)"
[cost]
input = 2.20
output = 13.20
cache_read = 0.22
cache_write = 2.75
input = 2.00
output = 12.00
cache_read = 0.20
cache_write = 2.50
[[cost.tiers]]
tier = { size = 272_000 }
input = 4.40
output = 19.80
cache_read = 0.44
cache_write = 5.50
input = 4.00
output = 18.00
cache_read = 0.40
cache_write = 5.00
@@ -1,3 +1,5 @@
# Mantle in-region only in us-east-1/us-east-2/us-west-2 (plus gov); pin us-east-1 so non-US AWS_REGION does not 404.
# Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-54.html
base_model = "openai/gpt-5.4"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
base_model_omit = ["cost.tiers", "cost.context_over_200k", "experimental.modes.fast", "limit.input"]
@@ -14,5 +16,5 @@ output = 128_000
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -1,3 +1,5 @@
# Mantle in-region only in us-east-1/us-east-2; pin us-east-1 so non-US AWS_REGION does not 404.
# Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-55.html
base_model = "openai/gpt-5.5"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
base_model_omit = ["cost.tiers", "cost.context_over_200k", "experimental.modes.fast", "limit.input"]
@@ -14,5 +16,5 @@ output = 128_000
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -1,6 +1,6 @@
# Mantle in-region only in us-east-1/us-east-2/us-west-2; pin us-east-1 so non-US AWS_REGION does not 404.
# Model card (regions, mantle URL, in-region pricing): https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html
# Bedrock model IDs and reasoning support: https://github.com/openai/codex/pull/30285
# Bedrock context window: https://developers.openai.com/api/docs/guides/amazon-bedrock
# In-region on-demand pricing (US East N. Virginia & Ohio; also US West Oregon): https://aws.amazon.com/bedrock/pricing/
# Columns: input / 30m cache write / cache read / output (USD per 1M tokens)
base_model = "openai/gpt-5.6-luna"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
@@ -21,5 +21,5 @@ cache_write = 0.55
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -1,6 +1,6 @@
# Mantle in-region only in us-east-1/us-east-2; pin us-east-1 so non-US AWS_REGION does not 404.
# Model card (regions, mantle URL, in-region pricing): https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html
# Bedrock model IDs and reasoning support: https://github.com/openai/codex/pull/30285
# Bedrock context window: https://developers.openai.com/api/docs/guides/amazon-bedrock
# In-region on-demand pricing (US East N. Virginia & Ohio): https://aws.amazon.com/bedrock/pricing/
# Columns: input / 30m cache write / cache read / output (USD per 1M tokens)
base_model = "openai/gpt-5.6-sol"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
@@ -21,5 +21,5 @@ cache_write = 13.75
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -1,6 +1,6 @@
# Mantle in-region only in us-east-1/us-east-2/us-west-2; pin us-east-1 so non-US AWS_REGION does not 404.
# Model card (regions, mantle URL, in-region pricing): https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html
# Bedrock model IDs and reasoning support: https://github.com/openai/codex/pull/30285
# Bedrock context window: https://developers.openai.com/api/docs/guides/amazon-bedrock
# In-region on-demand pricing (US East N. Virginia & Ohio; also US West Oregon): https://aws.amazon.com/bedrock/pricing/
# Columns: input / 30m cache write / cache read / output (USD per 1M tokens)
base_model = "openai/gpt-5.6-terra"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh", "max"] }]
@@ -21,5 +21,5 @@ cache_write = 5.50
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -1,3 +1,5 @@
# Mantle in-region only in us-east-1/us-east-2/us-west-2; pin us-east-1 so non-US AWS_REGION does not 404.
# Model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-3.html
base_model = "xai/grok-4.3"
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
last_updated = "2026-06-28"
@@ -16,5 +18,5 @@ input = ["text", "image"]
[provider]
npm = "@ai-sdk/amazon-bedrock/mantle"
api = "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1"
api = "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
shape = "responses"
@@ -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"
+3 -3
View File
@@ -7,9 +7,9 @@ type = "effort"
values = ["high", "max"]
[cost]
input = 0.6
output = 2
cache_read = 0.15
input = 1.05
output = 4.4
cache_read = 0.2
cache_write = 0
[limit]
@@ -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]
+3 -3
View File
@@ -6,9 +6,9 @@ type = "effort"
values = ["high", "max"]
[cost]
input = 0.6
output = 2
cache_read = 0.15
input = 1.05
output = 4.4
cache_read = 0.2
cache_write = 0
[limit]
-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.324 9.137l1.559 1.56h2.556v2.557L24 14.814V9.137zM2 9.52l-2 4.96h1.309l.37-.982H3.9l.408.982h1.338L3.432 9.52zm4.209 0v4.955h1.238v-3.092l1.338 1.562h.188l1.338-1.556v3.091h1.238V9.52H10.47l-1.592 1.845L7.287 9.52zm6.283 0v4.96h2.057c1.979 0 2.88-1.046 2.88-2.472 0-1.36-.937-2.488-2.747-2.488zm1.237.91h.792c1.17 0 1.63.711 1.63 1.57 0 .728-.372 1.572-1.616 1.572h-.806zm-10.985.273l.791 1.932H2.008zm17.137.307l-1.604 1.603v2.25h2.246l1.604-1.607h-2.246z"/>
</svg>

Before

Width:  |  Height:  |  Size: 562 B

@@ -1,16 +0,0 @@
base_model = "deepseek/deepseek-v4-flash-0731"
# Reasoning control verified against the AMD Token Factory API (accessed 2026-08-16):
# POST https://developer.amd.com.cn/radeon/api/v1/chat/completions validates
# reasoning_effort with enum none|minimal|low|medium|high|xhigh|max (invalid
# values return 400). thinking.* and budget controls are silently ignored and no
# reasoning_content is ever returned, so there is no toggle and no interleaved
# channel on this host.
reasoning_options = [
{ type = "effort", values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] },
]
[cost]
input = 0.14
output = 0.28
cache_read = 0.0028
-6
View File
@@ -1,6 +0,0 @@
name = "AMD"
env = ["AMD_API_KEY"]
npm = "@ai-sdk/openai-compatible"
# Token Factory (BETA) / Radeon Cloud, China endpoint
api = "https://developer.amd.com.cn/radeon/api/v1"
doc = "https://developer.amd.com.cn/radeon/tokenfactory"
-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd">
<path d="M13.236 2.377 2.751 20.493H0L11.863 0l1.373 2.377Zm3.554 6.156-9.606 11.96H4.13L15.511 6.32l1.279 2.212Zm6.908 11.96H14.05l8.406-2.151 1.242 2.15Zm-3.42-5.922-7.843 5.92H8.482l10.597-7.997 1.2 2.077Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 322 B

@@ -1,19 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cache hit: $0.028/MTok (Arcee correction, 2026-08-14).
# No toggle: thinking.type=enabled|disabled is rejected by the live API.
# Effort: reasoning_effort = high|max; low is rejected.
base_model = "deepseek/deepseek-v4-flash-0731"
name = "DeepSeek V4 Flash Latest"
status = "beta"
reasoning_options = [{ type = "effort", values = ["high", "max"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 0.14
output = 0.28
cache_read = 0.028
[limit]
context = 1_048_576
@@ -1,18 +0,0 @@
# Live inventory and pricing: https://api.arcee.ai/api/v1/models (verified 2026-08-17).
# This dated route is not yet listed on Arcee's public pricing page.
# No toggle: thinking.type=enabled|disabled is rejected by the live API.
# Effort: reasoning_effort = high|max; low is rejected.
base_model = "deepseek/deepseek-v4-pro-0813"
status = "beta"
reasoning_options = [{ type = "effort", values = ["high", "max"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 1.32
output = 3.96
cache_read = 0.044
[limit]
context = 1_048_576
@@ -1,18 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cached input pricing: https://docs.arcee.ai/get-started/pricing.md
# No toggle: thinking.type=enabled|disabled is rejected by the live API.
# Effort: reasoning_effort = high|max
base_model = "deepseek/deepseek-v4-pro"
status = "beta"
reasoning_options = [{ type = "effort", values = ["high", "max"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 1.74
output = 3.48
cache_read = 0.20
[limit]
context = 512_000
@@ -1,19 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cached input pricing: https://docs.arcee.ai/get-started/pricing.md
# Effort: reasoning_effort = low|high|max
base_model = "moonshotai/kimi-k3"
attachment = false
temperature = true
status = "beta"
reasoning_options = [{ type = "effort", values = ["low", "high", "max"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 3.00
output = 15.00
cache_read = 0.30
[modalities]
input = ["text"]
@@ -1,22 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cached input pricing: https://docs.arcee.ai/get-started/pricing.md
# A live reasoning_effort request is rejected; reasoning is always on.
base_model = "thinkingmachines/inkling-small"
attachment = false
status = "beta"
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.50
output = 1.20
cache_read = 0.10
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text"]
@@ -1,17 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cached input pricing: https://docs.arcee.ai/get-started/pricing.md
# A live reasoning_effort request is rejected; reasoning is always on.
base_model = "arcee-ai/trinity-large-thinking"
status = "beta"
reasoning_options = []
[interleaved]
field = "reasoning_content"
[cost]
input = 0.25
output = 0.80
cache_read = 0.06
[limit]
context = 262_144
@@ -1,17 +0,0 @@
# Beta catalog and pricing: https://www.arcee.ai/blog/open-models-api-beta
# Cached input pricing: https://docs.arcee.ai/get-started/pricing.md
# Effort: reasoning_effort = high|max
base_model = "zhipuai/glm-5.2"
status = "beta"
reasoning_options = [{ type = "effort", values = ["high", "max"] }]
[interleaved]
field = "reasoning_content"
[cost]
input = 1.40
output = 4.40
cache_read = 0.26
[limit]
context = 262_144
-5
View File
@@ -1,5 +0,0 @@
name = "Arcee"
npm = "@ai-sdk/openai-compatible"
env = ["ARCEE_API_KEY"]
api = "https://api.arcee.ai/api/v1"
doc = "https://docs.arcee.ai"
@@ -1,18 +0,0 @@
# Azure AI Foundry serves Claude Opus 4.7 at Anthropic list pricing:
# https://platform.claude.com/docs/en/about-claude/pricing
# Claude 4.6+ bills the full 1M context window at standard rates (no long-context
# premium), so no [[cost.tiers]] here. Model availability:
# https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/claude-models?tabs=pay-go#available-claude-models
base_model = "anthropic/claude-opus-4-7"
# Possible values described at https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[cost]
input = 5.00
output = 25.00
cache_read = 0.50
cache_write = 6.25
[provider]
npm = "@ai-sdk/anthropic"
api = "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1"
@@ -1,19 +0,0 @@
# Azure AI Foundry serves Claude Opus 4.7 at Anthropic list pricing:
# https://platform.claude.com/docs/en/about-claude/pricing
# Claude 4.6+ bills the full 1M context window at standard rates (no long-context
# premium), so no [[cost.tiers]] here — the tiers on azure's claude-opus-4-6/4-8
# entries predate that policy. Model availability:
# https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/claude-models?tabs=pay-go#available-claude-models
base_model = "anthropic/claude-opus-4-7"
# Possible values described at https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
[cost]
input = 5.00
output = 25.00
cache_read = 0.50
cache_write = 6.25
[provider]
npm = "@ai-sdk/anthropic"
api = "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1"
@@ -1,19 +1,16 @@
# Baseten documents top-level reasoning_effort = none | minimal | low | medium |
# high (default) | xhigh | max for DeepSeek V4 Flash 0731. Reasoning output is
# returned in reasoning_content.
# Baseten serves at most 384k output tokens for this model; keep this explicit
# because its catalog has reported the 1,048,576-token context as the output cap.
# Baseten's model matrix documents DeepSeek V4 Flash 0731 reasoning as "Enabled
# by default", but the model is absent from the "Control reasoning depth" table
# on the reasoning page, and that page warns that models outside it accept
# reasoning_effort and ignore it. So it reasons with no addressable depth.
# 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"
reasoning_options = []
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
[cost]
input = 0.13
output = 0.26
@@ -21,4 +18,4 @@ cache_read = 0.028
[limit]
context = 1_048_576
output = 384_000
output = 1_048_576
@@ -1,15 +1,13 @@
# Baseten documents top-level reasoning_effort = none | low | high | max for
# DeepSeek V4 Pro 0813. Reasoning output is returned in reasoning_content.
# Baseten documents top-level reasoning_effort = none | minimal | low |
# medium (default) | high | xhigh | max for DeepSeek V4 Pro.
# 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]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "high", "max"]
values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
[cost]
input = 1.32
@@ -2,7 +2,9 @@
# medium (default) | high | xhigh | max for DeepSeek V4 Pro; no toggle or
# 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"
@@ -17,5 +19,5 @@ output = 3.48
cache_read = 0.145
[limit]
context = 1_048_576
context = 262_144
output = 262_144
@@ -1,14 +1,9 @@
# Baseten documents opt-in via chat_template_args.enable_thinking=true, but no
# explicit false behavior, effort values, or reasoning-token budget. Reasoning
# output is returned in reasoning_content.
# explicit false behavior, effort values, or reasoning-token budget.
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "moonshotai/kimi-k2.7-code"
temperature = true
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "toggle"
@@ -1,15 +1,12 @@
# Baseten documents Kimi K3 reasoning as enabled by default, with top-level
# reasoning_effort = none | low | high | max (default). It also accepts
# chat_template_args.enable_thinking=false. No reasoning-token budget is
# documented. Reasoning output is returned in reasoning_content.
# documented.
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "moonshotai/kimi-k3"
description = "Kimi multimodal agent model for visual understanding, coding, and planning"
temperature = true
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "low", "high", "max"]
@@ -1,7 +1,6 @@
# Baseten now documents top-level reasoning_effort = none | minimal | low |
# medium (default) | high | xhigh | max for GPT OSS 120B; reasoning is
# otherwise enabled by default. No toggle or budget exists. Reasoning output is
# returned in reasoning_content.
# otherwise enabled by default. No toggle or budget exists.
# https://docs.baseten.co/inference/model-apis/reasoning
name = "OpenAI GPT 120B"
@@ -17,9 +16,6 @@ structured_output = true
knowledge = "2025-08"
open_weights = true
[interleaved]
field = "reasoning_content"
[[reasoning_options]]
type = "effort"
values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
@@ -7,7 +7,6 @@
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "zhipuai/glm-5.2"
name = "GLM 5.2 Fast"
attachment = true
[interleaved]
field = "reasoning_content"
@@ -24,6 +23,3 @@ cache_read = 0.21
[limit]
context = 1_048_576
output = 262_144
[modalities]
input = ["text", "image"]
@@ -5,7 +5,6 @@
# https://docs.baseten.co/inference/model-apis/reasoning
base_model = "zhipuai/glm-5.2"
name = "GLM 5.2"
attachment = true
[interleaved]
field = "reasoning_content"
@@ -22,6 +21,3 @@ cache_read = 0.3
[limit]
context = 1_048_576
output = 262_144
[modalities]
input = ["text", "image"]
@@ -0,0 +1,33 @@
name = "Z.AI GLM-4.7"
description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering"
# Model-specific reasoning HTTP values (accessed 2026-06-25):
# Reasoning defaults on; reasoning_effort = "none" disables it. The alternative
# disable_reasoning = true|false is deprecated and will be removed after
# 2026-07-21. No positive effort levels or token budget are documented.
# Sources:
# https://inference-docs.cerebras.ai/capabilities/reasoning#glm-reasoning-effort-and-disable-reasoning
# https://inference-docs.cerebras.ai/models/zai-glm-47
release_date = "2026-01-07"
last_updated = "2026-06-10"
attachment = false
reasoning = true
reasoning_options = [{ type = "effort", values = ["none"] }]
temperature = true
tool_call = true
open_weights = true
structured_output = true
status = "beta"
[cost]
input = 2.25
output = 2.75
cache_read = 2.25
cache_write = 0
[limit]
context = 131_072
output = 40_960
[modalities]
input = ["text"]
output = ["text"]
@@ -1,17 +0,0 @@
base_model = "alibaba/qwen3.8-27b"
name = "Qwen3.8 27B TEE"
description = "Qwen instruction model for multilingual chat, reasoning, and tool use"
release_date = "2026-08-16"
last_updated = "2026-08-16"
reasoning_options = []
[cost]
input = 0.35
output = 2.75
cache_read = 0.03499999999999999
[limit]
output = 65_536
[modalities]
input = ["text", "image"]
@@ -1,6 +1,6 @@
base_model = "openai/gpt-4o-mini"
[cost]
input = 0.075
output = 0.3
cache_read = 0.0375
input = 0.15
output = 0.6
cache_read = 0.075
@@ -1,6 +1,6 @@
base_model = "openai/gpt-4o"
[cost]
input = 1.25
output = 5
cache_read = 0.625
input = 2.5
output = 10
cache_read = 1.25
@@ -2,12 +2,7 @@
# Effort: reasoning_effort = high|max.
base_model = "deepseek/deepseek-v4-flash-0731"
[[reasoning_options]]
type = "toggle"
[[reasoning_options]]
type = "effort"
values = ["high", "max"]
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["high", "max"] }]
[cost]
input = 0.44
@@ -15,5 +10,5 @@ output = 1.32
cache_read = 0.014
[limit]
context = 1_310_720
context = 1_048_576
output = 1_048_576
@@ -1,19 +0,0 @@
# Workers AI exposes `reasoning_effort` and `chat_template_kwargs`; Qwen supports
# enable_thinking plus low|medium|xhigh effort levels.
# https://developers.cloudflare.com/workers-ai/models/qwen3.8-27b/ (accessed 2026-08-18)
# https://huggingface.co/Qwen/Qwen3.8-27B (accessed 2026-08-18)
base_model = "alibaba/qwen3.8-27b"
description = "Qwen vision-language model for visual reasoning, documents, and agent tasks"
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "xhigh"] }]
[cost]
input = 0.45
output = 3.2
cache_read = 0.05
[limit]
output = 262_144
[modalities]
input = ["text", "image"]
@@ -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"]

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