Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions models/jaredpalmer/kev-4b.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Source: https://huggingface.co/jaredpalmer/kev-4b (model card, accessed 2026-10-20).
# Kev 1.0, first published 2026-09-24: a LoRA adapter and pointer head on Qwen3.5-4B-Base that
# returns a probability per option and generates no text, so output is 0. States of up to
# 65,536 tokens are served; the card validates accuracy to 8,192. English only.
schema = 2
v2_only = true
type = "decision"
name = "Kev 4B"
description = "Jared Palmer's open-weight 4B decision model that returns calibrated probabilities for typed choice, score and yes/no questions about a text or JSON state"
open_weights = true
release_date = "2026-09-24"
license = "Apache-2.0"

[modalities]
input = ["text"]
output = ["decision"]

[capabilities]
tools = { supported = false }
reasoning = { supported = false }
temperature = false
top_p = false
top_k = false

[limit]
context = 65_536
output = 0

[[links]]
label = "Model card"
url = "https://huggingface.co/jaredpalmer/kev-4b"
type = "model_card"

[[links]]
label = "Source code"
url = "https://github.com/jaredpalmer/kev"
type = "other"

[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/jaredpalmer/kev-4b"
34 changes: 34 additions & 0 deletions models/vllm-sr/d3-mini.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Source: https://huggingface.co/vllm-sr/d3-mini (model card, config.json, decision_config.json; accessed
# 2026-10-21). Decision 3.0 from vLLM Semantic Router: a fine-tune of Qwen3.5-4B (4.54B parameters with a
# 0.33B vision encoder) that returns a probability per option and generates no text, so output is 0.
# Context: decision_config.json sets max_length = null, so the vLLM-SR runtime serves max_position_embeddings,
# 262,144 (vllm-project/semantic-router src/model-runtime/vllm_srun/families/decision3/package.py).
# Tag v3.0.0 was pushed on 2026-10-10 and the current weights, v3.1.0, on 2026-10-11.
schema = 2
v2_only = true
type = "decision"
name = "d3-mini"
description = "vLLM Semantic Router 4B decision model that answers typed choice, yes/no and score questions about text, images or video with probabilities"
open_weights = true
release_date = "2026-10-10"
last_updated = "2026-10-11"
license = "Apache-2.0"

[modalities]
input = ["text", "image", "video"]
output = ["decision"]

[capabilities]
tools = { supported = false }
reasoning = { supported = false }
temperature = false
top_p = false
top_k = false

[limit]
context = 262_144
output = 0

[[weights]]
label = "Hugging Face"
url = "https://huggingface.co/vllm-sr/d3-mini"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"cloudflare:sync": "bun ./packages/core/script/sync-models.ts cloudflare-workers-ai",
"chutes:sync": "bun ./packages/core/script/sync-models.ts chutes",
"engy:sync": "bun ./packages/core/script/sync-models.ts engy",
"verathos:sync": "bun ./packages/core/script/sync-models.ts verathos",
"databricks:generate": "bun ./packages/core/script/generate-databricks.ts",
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
"cloudflare-ai-gateway:generate": "bun ./packages/core/script/generate-cloudflare-ai-gateway.ts",
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ async function generateProviders(
cause: { providerPath },
});
}
let v2OnlyCount = 0;
for await (const modelPath of new Bun.Glob("**/*.toml").scan({
cwd: modelsPath,
absolute: true,
Expand All @@ -148,7 +149,10 @@ async function generateProviders(
}).then((mod) => mod.default);
raw.id = modelID;
// Models marked `v2_only = true` are only published in the v2 API.
if (raw.v2_only === true) continue;
if (raw.v2_only === true) {
v2OnlyCount++;
continue;
}
const toml = modelV2ToV1(raw, { modelPath }, provider.data.npm);
if (toml.base_model !== undefined) {
const baseModel = BaseModel.safeParse(toml);
Expand Down Expand Up @@ -177,6 +181,8 @@ async function generateProviders(
provider.data.models[modelID] = normalizeModelCost(model.data);
}
if (Object.keys(provider.data.models).length === 0) {
// A provider that serves only v2_only models is left out of v1.
if (v2OnlyCount > 0) continue;
throw new Error(`Provider "${providerID}" has no models`, {
cause: { providerPath },
});
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { requesty } from "./providers/requesty.js";
import { tinfoil } from "./providers/tinfoil.js";
import { vercel } from "./providers/vercel.js";
import { venice } from "./providers/venice.js";
import { verathos } from "./providers/verathos.js";
import { wandb } from "./providers/wandb.js";
import { xai } from "./providers/xai.js";

Expand Down Expand Up @@ -224,6 +225,7 @@ export const providers: {
tinfoil: SyncProvider<any>;
vercel: SyncProvider<any>;
venice: SyncProvider<any>;
verathos: SyncProvider<any>;
wandb: SyncProvider<any>;
xai: SyncProvider<any>;
} = {
Expand Down Expand Up @@ -269,6 +271,7 @@ export const providers: {
tinfoil,
vercel,
venice,
verathos,
wandb,
xai,
};
Expand All @@ -294,7 +297,7 @@ export const groups = {
"vercel",
],
cloudflare: ["cloudflare-ai-gateway", "cloudflare-workers-ai"],
direct: ["aiand", "ambient", "anthropic", "baseten", "chutes", "cortecs", "deepinfra", "digitalocean", "engy", "fireworks-ai", "friendli", "github-copilot", "google", "hyper", "meta", "mistral", "nearai", "ollama-cloud", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
direct: ["aiand", "ambient", "anthropic", "baseten", "chutes", "cortecs", "deepinfra", "digitalocean", "engy", "fireworks-ai", "friendli", "github-copilot", "google", "hyper", "meta", "mistral", "nearai", "ollama-cloud", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "verathos", "wandb", "xai"],
} as const;

type ProviderID = keyof typeof providers;
Expand Down
162 changes: 162 additions & 0 deletions packages/core/src/sync/providers/verathos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { z } from "zod";

import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js";

const API_ENDPOINT = "https://api.verathos.ai/v1/models";

// USD per 1M tokens. A missing or negative price must fail the run, not be
// written as a free model.
const Price = z
.object({
usd_per_million_input_tokens: z.number().nonnegative(),
usd_per_million_output_tokens: z.number().nonnegative(),
})
.passthrough();

export const VerathosModel = z
.object({
id: z.string(),
max_state_tokens: z.number().int().optional(),
input_modalities: z.array(z.string()).nullish(),
price: Price.optional(),
available: z.boolean().optional(),
})
.passthrough();

export const VerathosResponse = z.object({ data: z.array(VerathosModel) }).passthrough();

export type VerathosModel = z.infer<typeof VerathosModel>;

type Modality = "text" | "audio" | "image" | "video" | "pdf";

export const verathos = {
id: "verathos",
name: "Verathos",
modelsDir: "providers/verathos/models",
// The list has no name, description or lab facts, and every model needs a
// decision-type lab entry first. Unseen ids are reported for a human to author.
skipCreates: true,
trackMissingModels: true,
// The list is unauthenticated and an empty `data` passes the schema; one
// truncated 200 must not delete the authored files.
deleteMissing: false,
sourceID(model) {
return model.id;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} Verathos models have no local catalog file and were not created because the list carries no lab metadata; author a decision-type lab entry and a provider file: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
missingNotice(paths) {
if (paths.length === 0) return [];
return [
`${paths.length} local Verathos models are missing from the public list and were retained; a human removes retired models: ${paths.map((path) => `\`${path}\``).join(", ")}`,
];
},
async fetchModels() {
return fetchVerathosModels();
},
parseModels(raw) {
return parseVerathosModels(raw);
},
translateModel(model, context) {
return {
id: model.id,
model: buildVerathosModel(model, context.existing(model.id), context.authored(model.id)),
};
},
} satisfies SyncProvider<VerathosModel>;

export async function fetchVerathosModels(fetcher: typeof fetch = fetch) {
const response = await fetcher(API_ENDPOINT);
if (!response.ok) {
throw new Error(`Verathos models request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}

export function parseVerathosModels(raw: unknown): VerathosModel[] {
const rows = VerathosResponse.parse(raw).data;
// A row without an id maps to no file and would open a nameless missing-model issue.
const unnamed = rows.filter((model) => model.id.trim() === "").length;
if (unnamed > 0) console.warn(`verathos: ignored ${unnamed} model row(s) with an empty id`);
return rows.filter((model) => model.id.trim() !== "");
}

export function buildVerathosModel(
model: VerathosModel,
existing: ExistingModel | undefined,
authored?: ExistingModel,
): SyncedModel {
// An absent, empty or unrecognised modality list means "not reported", not text-only.
const input = normalizeModalities(model.input_modalities) ?? existing?.modalities?.input ?? ["text"];
const output = existing?.modalities?.output ?? ["text"];

// max_state_tokens is the largest state Verathos accepts (413 above it). A
// non-positive value is a bad row, not a smaller window.
const apiContext = model.max_state_tokens ?? 0;
const limit = {
context: apiContext > 0 ? apiContext : existing?.limit?.context ?? 0,
input: existing?.limit?.input,
// Decision models generate no text.
output: existing?.limit?.output ?? 0,
};

const cost =
model.price === undefined
? existing?.cost
: {
...existing?.cost,
input: round(model.price.usd_per_million_input_tokens),
output: round(model.price.usd_per_million_output_tokens),
};

// Keep every authored field and overwrite only what the list is authoritative for.
const { base_model: authoredBase, base_model_omit: baseModelOmit, ...current } = existing ?? {};
const values = {
...current,
name: current.name ?? model.id,
attachment: input.some((value) => value !== "text"),
cost,
limit,
modalities: { input, output },
} as Parameters<typeof factorBaseModel>[1];

// An authored pointer wins: a resolver miss must not de-factor a committed file.
const baseModel = authoredBase ?? resolveModelMetadataBaseModel(model.id);
if (baseModel === undefined) return values as SyncedModel;
const factored = factorBaseModel(baseModel, values, limit, baseModelOmit);
// A v2 file's modalities (MIME types, output "decision") read as plain v1 values
// equal to the lab's, so factoring drops them and the runner would delete the
// table. While the list agrees with the authored values, keep them.
if (authored?.modalities !== undefined && sameJSON(authored.modalities, values.modalities)) {
return { ...factored, attachment: authored.attachment, modalities: authored.modalities } as SyncedModel;
}
return factored;
}

function sameJSON(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}

// Prices are already per-1M; round to micro-dollars so a float in the JSON
// never rewrites a file.
function round(value: number): number {
return Math.round(value * 1e6) / 1e6;
}

function normalizeModalities(values: string[] | null | undefined): Modality[] | undefined {
if (values === undefined || values === null) return undefined;
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
const result = values
.map((value) => value.toLowerCase())
.filter((value): value is Modality => allowed.has(value as Modality));
const unique = [...new Set(result)];
if (unique.length === 0) return undefined;
// Wire order is arbitrary; sort so a rewrite for any other reason keeps catalogue order.
const order: Modality[] = ["text", "image", "audio", "video", "pdf"];
return unique.sort((a, b) => order.indexOf(a) - order.indexOf(b));
}
45 changes: 45 additions & 0 deletions packages/core/test/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,51 @@ describe("catalog generation", () => {
});
});

test("leaves providers with only v2_only models out of v1", async () => {
await withFixture(async (root) => {
await write(root, "providers/decider/provider.toml", providerToml("Decider"));
await write(
root,
"providers/decider/models/decide.toml",
`schema = 2
v2_only = true
type = "decision"
name = "Decide"
description = "Example decision model that only the v2 catalog publishes"
open_weights = true
release_date = "2026-01-02"

[modalities]
input = ["text"]
output = ["decision"]

[capabilities]
tools = { supported = false }
reasoning = { supported = false }
temperature = false
top_p = false
top_k = false

[limit]
context = 8_192
output = 0

[cost.text.input]
price = 1

[cost.text.output]
price = 0
`,
);

const providers = await generate(path.join(root, "providers"));
expect(providers.decider).toBeUndefined();

const providersV2 = await generateV2(path.join(root, "providers"));
expect(Object.keys(providersV2.decider?.models ?? {})).toEqual(["decide"]);
});
});

test("base_model publishes canonical identity with the provider model", async () => {
await withFixture(async (root) => {
await write(root, "providers/direct/provider.toml", providerToml("Direct"));
Expand Down
Loading
Loading