From e63bf80dbc4e57fecec4b4ecc06c16098b557bfe Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Mon, 14 Sep 2026 18:07:39 +0800 Subject: [PATCH 01/17] feat(sync): add Novita AI model catalog sync --- .github/workflows/sync-models.yml | 1 + packages/core/src/sync/index.ts | 4 + packages/core/src/sync/providers/novita-ai.ts | 75 ++++++++ packages/core/test/novita-ai.test.ts | 169 ++++++++++++++++++ providers/novita-ai/logo.svg | 11 +- providers/novita-ai/provider.toml | 2 +- 6 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/sync/providers/novita-ai.ts create mode 100644 packages/core/test/novita-ai.test.ts diff --git a/.github/workflows/sync-models.yml b/.github/workflows/sync-models.yml index ac9f3690bcd..850d83b6d1a 100644 --- a/.github/workflows/sync-models.yml +++ b/.github/workflows/sync-models.yml @@ -83,6 +83,7 @@ jobs: VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }} LLMGATEWAY_API_KEY: ${{ secrets.LLMGATEWAY_API_KEY }} MERGE_GATEWAY_API_KEY: ${{ secrets.MERGE_GATEWAY_API_KEY }} + NOVITA_API_KEY: ${{ secrets.NOVITA_API_KEY }} KILO_API_KEY: ${{ secrets.KILO_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index e6c30874235..1d06bf0b10f 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -28,6 +28,7 @@ import { llmgateway, llmgatewayProviders } from "./providers/llmgateway.js"; import { mergeGateway } from "./providers/merge-gateway.js"; import { meta } from "./providers/meta.js"; import { nanoGpt } from "./providers/nano-gpt.js"; +import { novitaAi } from "./providers/novita-ai.js"; import { ollamaCloud } from "./providers/ollama-cloud.js"; import { openai } from "./providers/openai.js"; import { ofox } from "./providers/ofox.js"; @@ -154,6 +155,7 @@ export const providers: { "merge-gateway": SyncProvider; meta: SyncProvider; "nano-gpt": SyncProvider; + "novita-ai": SyncProvider; ofox: SyncProvider; "ollama-cloud": SyncProvider; openai: SyncProvider; @@ -190,6 +192,7 @@ export const providers: { "merge-gateway": mergeGateway, meta, "nano-gpt": nanoGpt, + "novita-ai": novitaAi, ofox, "ollama-cloud": ollamaCloud, openai, @@ -216,6 +219,7 @@ export const groups = { "llmgateway-providers", "merge-gateway", "nano-gpt", + "novita-ai", "ofox", "requesty", "openrouter", diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts new file mode 100644 index 00000000000..caf8fcab606 --- /dev/null +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; + +import { AuthoredModel } from "../../schema.js"; +import type { ExistingModel, SyncProvider, SyncedBaseModel, SyncedModel } from "../index.js"; + +const API_ENDPOINT = "https://api.novita.ai/openai/v1/models"; + +export const NovitaAIModel = z.object({ + id: z.string().min(1), + object: z.literal("model"), + created: z.number().int().nonnegative(), + owned_by: z.string(), +}).passthrough(); + +export const NovitaAIResponse = z.object({ + object: z.literal("list"), + data: z.array(NovitaAIModel), +}).passthrough(); + +export type NovitaAIModel = z.infer; + +function preserveAuthoredModel(id: string, authored: ExistingModel): SyncedModel { + if (authored.base_model !== undefined) return authored as SyncedBaseModel; + + const parsed = AuthoredModel.safeParse({ id, ...authored }); + if (!parsed.success) { + parsed.error.cause = { provider: "novita-ai", model: id }; + throw parsed.error; + } + const { id: _id, ...model } = parsed.data; + return model; +} + +export async function fetchNovitaAIModels(key: string, fetcher: typeof fetch = fetch) { + const response = await fetcher(API_ENDPOINT, { + method: "GET", + headers: { Authorization: `Bearer ${key}` }, + }); + if (!response.ok) { + throw new Error(`Novita AI models request failed: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +export const novitaAi = { + id: "novita-ai", + name: "Novita AI", + modelsDir: "providers/novita-ai/models", + skipCreates: true, + deleteMissing: false, + sourceID(model) { + return model.id; + }, + skippedNotice(ids) { + if (ids.length === 0) return []; + return [ + `${ids.length} Novita AI models returned by the API were not created because the catalog requires hand-authored metadata for new models.`, + `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, + ]; + }, + async fetchModels() { + const key = process.env.NOVITA_API_KEY; + if (key === undefined) throw new Error("Novita AI sync requires NOVITA_API_KEY"); + return fetchNovitaAIModels(key); + }, + parseModels(raw) { + return NovitaAIResponse.parse(raw).data; + }, + translateModel(model, context) { + const authored = context.authored(model.id); + if (authored === undefined) return undefined; + return { id: model.id, model: preserveAuthoredModel(model.id, authored) }; + }, +} satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts new file mode 100644 index 00000000000..0ba54706283 --- /dev/null +++ b/packages/core/test/novita-ai.test.ts @@ -0,0 +1,169 @@ +import { expect, test } from "bun:test"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { groups, providers, syncProvider } from "../src/sync/index.js"; +import { fetchNovitaAIModels, NovitaAIResponse, novitaAi, type NovitaAIModel } from "../src/sync/providers/novita-ai.js"; + +function novitaAiModel(overrides: Partial = {}): NovitaAIModel { + return { + id: "deepseek/deepseek-v3.2", + object: "model", + created: 1_765_440_000, + owned_by: "novita", + ...overrides, + }; +} + +test("parses Novita AI API response", () => { + const parsed = NovitaAIResponse.parse({ + object: "list", + data: [ + novitaAiModel(), + novitaAiModel({ id: "meta-llama/llama-3.3-70b-instruct", created: 1_733_635_200 }), + ], + }); + expect(parsed.data).toHaveLength(2); + expect(parsed.data[0]?.id).toBe("deepseek/deepseek-v3.2"); + expect(parsed.data[1]?.id).toBe("meta-llama/llama-3.3-70b-instruct"); +}); + +test("rejects invalid Novita AI API responses", () => { + expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "bad", object: "not-model", created: 1, owned_by: "" }] })) + .toThrow(); + expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "", object: "model", created: -1, owned_by: "" }] })) + .toThrow(); +}); + +test("Novita AI sync preserves authored metadata for existing models", () => { + const authored = { + base_model: "deepseek/deepseek-v3.2", + name: "Deepseek V3.2", + description: "DeepSeek chat model for instruction following, coding, and analysis", + family: "deepseek", + release_date: "2025-12-01", + last_updated: "2025-12-01", + attachment: false, + reasoning: true, + reasoning_options: [{ type: "toggle" } as const], + temperature: true, + tool_call: true, + structured_output: true, + open_weights: true, + cost: { input: 0.269, output: 0.4, cache_read: 0.1345 }, + limit: { context: 163_840, output: 65_536 }, + interleaved: { field: "reasoning_content" }, + modalities: { input: ["text"], output: ["text"] }, + }; + + const translated = novitaAi.translateModel(novitaAiModel(), { + existing: () => authored, + authored: () => authored, + }); + + expect(translated).toEqual({ id: "deepseek/deepseek-v3.2", model: authored }); +}); + +test("Novita AI sync skips unknown remote models", () => { + expect(novitaAi.translateModel(novitaAiModel({ id: "novita/unknown-model" }), { + existing: () => undefined, + authored: () => undefined, + })).toBeUndefined(); +}); + +test("Novita AI sync retains local models absent from API response", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-ai-")); + const modelsDir = path.join(dir, "providers", "novita-ai", "models"); + await mkdir(modelsDir, { recursive: true }); + await Bun.write(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml"), [ + 'name = "Deepseek V3.2"', + 'description = "DeepSeek chat model for instruction following, coding, and analysis"', + 'family = "deepseek"', + 'release_date = "2025-12-01"', + 'last_updated = "2025-12-01"', + "attachment = false", + "reasoning = true", + "reasoning_options = [{ type = \"toggle\" }]", + "temperature = true", + "tool_call = true", + "structured_output = true", + "open_weights = true", + "", + "[interleaved]", + 'field = "reasoning_content"', + "", + "[cost]", + "input = 0.269", + "output = 0.4", + "cache_read = 0.1345", + "", + "[limit]", + "context = 163_840", + "output = 65_536", + "", + "[modalities]", + 'input = ["text"]', + 'output = ["text"]', + "", + ].join("\n")); + + try { + const result = await syncProvider({ + ...novitaAi, + modelsDir, + async fetchModels() { + return { + object: "list", + data: [novitaAiModel({ id: "meta-llama/llama-3.3-70b-instruct" })], + }; + }, + }); + expect(result.deleted).toBe(0); + expect(result.unchanged).toBe(1); + expect(await Bun.file(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml")).exists()).toBe(true); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("Novita AI sync tracks remote-only IDs", () => { + expect(providers["novita-ai"]).toBe(novitaAi); + expect(groups.aggregators).toContain("novita-ai"); + expect(novitaAi.sourceID?.(novitaAiModel())).toBe("deepseek/deepseek-v3.2"); + expect(novitaAi.sourceID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); +}); + +test("Novita AI sync requires NOVITA_API_KEY", async () => { + const original = process.env.NOVITA_API_KEY; + delete process.env.NOVITA_API_KEY; + try { + await expect(novitaAi.fetchModels()).rejects.toThrow("Novita AI sync requires NOVITA_API_KEY"); + } finally { + if (original !== undefined) process.env.NOVITA_API_KEY = original; + } +}); + +test("fetchNovitaAIModels passes Authorization header", async () => { + let request: Request | undefined; + const fetcher = async (_url: string, _init?: RequestInit) => { + request = new Request(_url, _init); + return new Response(JSON.stringify({ object: "list", data: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + const result = await fetchNovitaAIModels("test-key", fetcher); + expect(result).toEqual({ object: "list", data: [] }); + expect(request?.method).toBe("GET"); + expect(request?.url).toBe("https://api.novita.ai/openai/v1/models"); + expect(request?.headers.get("authorization")).toBe("Bearer test-key"); +}); + +test("fetchNovitaAIModels throws on HTTP error", async () => { + const fetcher = async () => + new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }); + + await expect(fetchNovitaAIModels("bad-key", fetcher)).rejects.toThrow("401 Unauthorized"); +}); diff --git a/providers/novita-ai/logo.svg b/providers/novita-ai/logo.svg index ac537b8dd42..b776ce6a4f5 100644 --- a/providers/novita-ai/logo.svg +++ b/providers/novita-ai/logo.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + diff --git a/providers/novita-ai/provider.toml b/providers/novita-ai/provider.toml index b7ebc291683..2af00eda81b 100644 --- a/providers/novita-ai/provider.toml +++ b/providers/novita-ai/provider.toml @@ -1,4 +1,4 @@ -name = "NovitaAI" +name = "Novita AI" env = ["NOVITA_API_KEY"] npm = "@ai-sdk/openai-compatible" # Raw HTTP reasoning controls (sources accessed 2026-06-25): From b3eb982a46d138812692c69c988d3634deef6653 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Tue, 15 Sep 2026 17:53:55 +0800 Subject: [PATCH 02/17] fix(sync): accept Novita model list response --- packages/core/src/sync/providers/novita-ai.ts | 4 +++- packages/core/test/novita-ai.test.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index caf8fcab606..15168242d47 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -13,7 +13,9 @@ export const NovitaAIModel = z.object({ }).passthrough(); export const NovitaAIResponse = z.object({ - object: z.literal("list"), + // Novita's endpoint currently omits the OpenAI-compatible top-level object. + // Keep accepting the standard value if the API adds it later. + object: z.literal("list").optional(), data: z.array(NovitaAIModel), }).passthrough(); diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 0ba54706283..cf6df6fc248 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -18,7 +18,6 @@ function novitaAiModel(overrides: Partial = {}): NovitaAIModel { test("parses Novita AI API response", () => { const parsed = NovitaAIResponse.parse({ - object: "list", data: [ novitaAiModel(), novitaAiModel({ id: "meta-llama/llama-3.3-70b-instruct", created: 1_733_635_200 }), @@ -29,6 +28,10 @@ test("parses Novita AI API response", () => { expect(parsed.data[1]?.id).toBe("meta-llama/llama-3.3-70b-instruct"); }); +test("accepts the standard OpenAI list marker when present", () => { + expect(NovitaAIResponse.parse({ object: "list", data: [novitaAiModel()] }).data).toHaveLength(1); +}); + test("rejects invalid Novita AI API responses", () => { expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "bad", object: "not-model", created: 1, owned_by: "" }] })) .toThrow(); From ffde31bd7704f61a253b4d4c04540b085e90b195 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 11:26:04 +0800 Subject: [PATCH 03/17] feat(sync): map Novita catalog metadata --- packages/core/src/sync/providers/novita-ai.ts | 59 ++++++++++++++++++- packages/core/test/novita-ai.test.ts | 21 +++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 15168242d47..332eaa46bed 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -10,6 +10,19 @@ export const NovitaAIModel = z.object({ object: z.literal("model"), created: z.number().int().nonnegative(), owned_by: z.string(), + title: z.string().optional(), + display_name: z.string().optional(), + description: z.string().optional(), + context_size: z.number().int().positive().optional(), + max_output_tokens: z.number().int().positive().optional(), + features: z.array(z.string()).optional(), + input_modalities: z.array(z.string()).optional(), + output_modalities: z.array(z.string()).optional(), + pricing: z.object({ + prompt: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), + completion: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), + input_cache_read: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), + }).passthrough().optional(), }).passthrough(); export const NovitaAIResponse = z.object({ @@ -33,6 +46,19 @@ function preserveAuthoredModel(id: string, authored: ExistingModel): SyncedModel return model; } +function decimalPrice(value: string | undefined) { + if (value === undefined) return undefined; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; +} + +function modalities(values: string[] | undefined, fallback: ExistingModel["modalities"] | undefined) { + if (values === undefined || values.length === 0) return fallback; + const allowed = new Set(["text", "audio", "image", "video", "pdf"]); + const result = values.map((value) => value.toLowerCase()).filter((value) => allowed.has(value)); + return result.length > 0 ? [...new Set(result)] : fallback; +} + export async function fetchNovitaAIModels(key: string, fetcher: typeof fetch = fetch) { const response = await fetcher(API_ENDPOINT, { method: "GET", @@ -72,6 +98,37 @@ export const novitaAi = { translateModel(model, context) { const authored = context.authored(model.id); if (authored === undefined) return undefined; - return { id: model.id, model: preserveAuthoredModel(model.id, authored) }; + const translated = { ...preserveAuthoredModel(model.id, authored) } as Record; + if (model.display_name ?? model.title) translated.name = model.display_name ?? model.title; + if (model.description) translated.description = model.description; + if (model.context_size !== undefined || model.max_output_tokens !== undefined) { + translated.limit = { + ...authored.limit, + ...(model.context_size !== undefined ? { context: model.context_size } : {}), + ...(model.max_output_tokens !== undefined ? { output: model.max_output_tokens } : {}), + }; + } + const input = modalities(model.input_modalities, authored.modalities?.input); + const output = modalities(model.output_modalities, authored.modalities?.output); + if (input !== undefined && output !== undefined) translated.modalities = { input, output }; + const features = new Set(model.features ?? []); + if (model.features !== undefined) { + translated.reasoning = features.has("reasoning"); + translated.tool_call = features.has("function-calling"); + translated.structured_output = features.has("structured-outputs"); + } + const pricing = model.pricing; + const inputCost = decimalPrice(pricing?.prompt?.price_per_m_decimal); + const outputCost = decimalPrice(pricing?.completion?.price_per_m_decimal); + const cacheRead = decimalPrice(pricing?.input_cache_read?.price_per_m_decimal); + if (inputCost !== undefined || outputCost !== undefined || cacheRead !== undefined) { + translated.cost = { + ...authored.cost, + ...(inputCost !== undefined ? { input: inputCost } : {}), + ...(outputCost !== undefined ? { output: outputCost } : {}), + ...(cacheRead !== undefined ? { cache_read: cacheRead } : {}), + }; + } + return { id: model.id, model: translated as SyncedModel }; }, } satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index cf6df6fc248..a6b8109552a 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -32,6 +32,27 @@ test("accepts the standard OpenAI list marker when present", () => { expect(NovitaAIResponse.parse({ object: "list", data: [novitaAiModel()] }).data).toHaveLength(1); }); +test("maps Novita catalog metadata onto existing models", () => { + const translated = novitaAi.translateModel(novitaAiModel({ + display_name: "GLM 5.3 Flash", + description: "Updated description", + context_size: 1_048_576, + max_output_tokens: 131_072, + features: ["function-calling", "structured-outputs", "reasoning"], + input_modalities: ["text", "image"], + output_modalities: ["text"], + pricing: { + prompt: { price_per_m_decimal: "0.15" }, + completion: { price_per_m_decimal: "0.5" }, + input_cache_read: { price_per_m_decimal: "0.03" }, + }, + }), { + existing: () => ({}), + authored: () => ({ base_model: "test/base", name: "Old", description: "Old", attachment: false, reasoning: false, tool_call: false, open_weights: true, limit: { context: 1, output: 1 }, modalities: { input: ["text"], output: ["text"] } }), + }); + expect(translated?.model).toMatchObject({ name: "GLM 5.3 Flash", reasoning: true, tool_call: true, structured_output: true, limit: { context: 1_048_576, output: 131_072 }, cost: { input: 0.15, output: 0.5, cache_read: 0.03 }, modalities: { input: ["text", "image"], output: ["text"] } }); +}); + test("rejects invalid Novita AI API responses", () => { expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "bad", object: "not-model", created: 1, owned_by: "" }] })) .toThrow(); From 010bff0fcdaf59a8decf4e0de1bd5763a27bba20 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 12:17:14 +0800 Subject: [PATCH 04/17] feat(sync): automatically sync Novita model metadata --- packages/core/src/sync/providers/novita-ai.ts | 127 ++++++++++-------- packages/core/test/novita-ai.test.ts | 10 +- 2 files changed, 74 insertions(+), 63 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 332eaa46bed..63c2b088688 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -1,9 +1,12 @@ import { z } from "zod"; +import path from "node:path"; -import { AuthoredModel } from "../../schema.js"; -import type { ExistingModel, SyncProvider, SyncedBaseModel, SyncedModel } from "../index.js"; +import { describeModel } from "../../describe.js"; +import { inferKimiFamily, ModelFamilyValues } from "../../family.js"; +import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; const API_ENDPOINT = "https://api.novita.ai/openai/v1/models"; +const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models"); export const NovitaAIModel = z.object({ id: z.string().min(1), @@ -34,31 +37,76 @@ export const NovitaAIResponse = z.object({ export type NovitaAIModel = z.infer; -function preserveAuthoredModel(id: string, authored: ExistingModel): SyncedModel { - if (authored.base_model !== undefined) return authored as SyncedBaseModel; - - const parsed = AuthoredModel.safeParse({ id, ...authored }); - if (!parsed.success) { - parsed.error.cause = { provider: "novita-ai", model: id }; - throw parsed.error; - } - const { id: _id, ...model } = parsed.data; - return model; -} - function decimalPrice(value: string | undefined) { if (value === undefined) return undefined; const parsed = Number(value); return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; } -function modalities(values: string[] | undefined, fallback: ExistingModel["modalities"] | undefined) { +type Modality = "text" | "audio" | "image" | "video" | "pdf"; + +function modalities(values: string[] | undefined, fallback: Modality[] | undefined) { if (values === undefined || values.length === 0) return fallback; - const allowed = new Set(["text", "audio", "image", "video", "pdf"]); - const result = values.map((value) => value.toLowerCase()).filter((value) => allowed.has(value)); + const allowed = new Set(["text", "audio", "image", "video", "pdf"]); + const result = values + .map((value) => value.toLowerCase() === "file" ? "pdf" : value.toLowerCase()) + .filter((value): value is Modality => allowed.has(value as Modality)); return result.length > 0 ? [...new Set(result)] : fallback; } +function dateFromTimestamp(timestamp: number) { + return new Date(timestamp * 1000).toISOString().slice(0, 10); +} + +function inferFamily(id: string, name: string) { + const kimi = inferKimiFamily(id, name); + if (kimi !== undefined) return kimi; + const target = `${id} ${name}`.toLowerCase(); + return [...ModelFamilyValues].sort((a, b) => b.length - a.length).find((family) => + new RegExp(`(^|[^a-z0-9])${family.toLowerCase().replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}(?=$|[^a-z0-9])`).test(target)); +} + +function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefined): SyncedModel { + const name = model.display_name ?? model.title ?? existing?.name ?? model.id; + const input = modalities(model.input_modalities, existing?.modalities?.input) ?? ["text"]; + const output = modalities(model.output_modalities, existing?.modalities?.output) ?? ["text"]; + const features = model.features === undefined ? undefined : new Set(model.features); + const reasoning = features?.has("reasoning") ?? existing?.reasoning ?? false; + const toolCall = features?.has("function-calling") ?? existing?.tool_call ?? false; + const structuredOutput = features?.has("structured-outputs") ?? existing?.structured_output ?? false; + const context = model.context_size ?? existing?.limit?.context ?? 0; + const outputLimit = model.max_output_tokens ?? existing?.limit?.output ?? context; + const inputCost = decimalPrice(model.pricing?.prompt?.price_per_m_decimal); + const outputCost = decimalPrice(model.pricing?.completion?.price_per_m_decimal); + const cacheRead = decimalPrice(model.pricing?.input_cache_read?.price_per_m_decimal); + const cost = inputCost !== undefined && outputCost !== undefined + ? { input: inputCost, output: outputCost, cache_read: cacheRead } + : existing?.cost ?? { input: 0, output: 0 }; + const values: SyncedFullModel = { + name, + description: model.description ?? existing?.description ?? describeModel({ id: model.id, name, family: inferFamily(model.id, name), reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? true, limit: { context, output: outputLimit }, modalities: { input, output } }), + family: existing?.family ?? inferFamily(model.id, name), + release_date: existing?.release_date ?? dateFromTimestamp(model.created), + last_updated: existing?.last_updated ?? dateFromTimestamp(model.created), + attachment: input.some((value) => value !== "text"), + reasoning, + tool_call: toolCall, + structured_output: structuredOutput, + temperature: existing?.temperature ?? true, + open_weights: existing?.open_weights ?? true, + cost, + limit: { context, output: outputLimit }, + modalities: { input, output }, + }; + if (existing?.base_model === undefined) return values; + return { + ...existing, + ...values, + base_model: existing.base_model, + ...(existing.base_model_omit === undefined ? {} : { base_model_omit: existing.base_model_omit }), + } as SyncedModel; +} + export async function fetchNovitaAIModels(key: string, fetcher: typeof fetch = fetch) { const response = await fetcher(API_ENDPOINT, { method: "GET", @@ -75,18 +123,12 @@ export const novitaAi = { id: "novita-ai", name: "Novita AI", modelsDir: "providers/novita-ai/models", - skipCreates: true, + // The endpoint exposes the metadata needed to author new provider models. + skipCreates: false, deleteMissing: false, sourceID(model) { return model.id; }, - skippedNotice(ids) { - if (ids.length === 0) return []; - return [ - `${ids.length} Novita AI models returned by the API were not created because the catalog requires hand-authored metadata for new models.`, - `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, - ]; - }, async fetchModels() { const key = process.env.NOVITA_API_KEY; if (key === undefined) throw new Error("Novita AI sync requires NOVITA_API_KEY"); @@ -96,39 +138,6 @@ export const novitaAi = { return NovitaAIResponse.parse(raw).data; }, translateModel(model, context) { - const authored = context.authored(model.id); - if (authored === undefined) return undefined; - const translated = { ...preserveAuthoredModel(model.id, authored) } as Record; - if (model.display_name ?? model.title) translated.name = model.display_name ?? model.title; - if (model.description) translated.description = model.description; - if (model.context_size !== undefined || model.max_output_tokens !== undefined) { - translated.limit = { - ...authored.limit, - ...(model.context_size !== undefined ? { context: model.context_size } : {}), - ...(model.max_output_tokens !== undefined ? { output: model.max_output_tokens } : {}), - }; - } - const input = modalities(model.input_modalities, authored.modalities?.input); - const output = modalities(model.output_modalities, authored.modalities?.output); - if (input !== undefined && output !== undefined) translated.modalities = { input, output }; - const features = new Set(model.features ?? []); - if (model.features !== undefined) { - translated.reasoning = features.has("reasoning"); - translated.tool_call = features.has("function-calling"); - translated.structured_output = features.has("structured-outputs"); - } - const pricing = model.pricing; - const inputCost = decimalPrice(pricing?.prompt?.price_per_m_decimal); - const outputCost = decimalPrice(pricing?.completion?.price_per_m_decimal); - const cacheRead = decimalPrice(pricing?.input_cache_read?.price_per_m_decimal); - if (inputCost !== undefined || outputCost !== undefined || cacheRead !== undefined) { - translated.cost = { - ...authored.cost, - ...(inputCost !== undefined ? { input: inputCost } : {}), - ...(outputCost !== undefined ? { output: outputCost } : {}), - ...(cacheRead !== undefined ? { cache_read: cacheRead } : {}), - }; - } - return { id: model.id, model: translated as SyncedModel }; + return { id: model.id, model: buildNovitaModel(model, context.authored(model.id)) }; }, } satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index a6b8109552a..5af38165649 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -86,14 +86,16 @@ test("Novita AI sync preserves authored metadata for existing models", () => { authored: () => authored, }); - expect(translated).toEqual({ id: "deepseek/deepseek-v3.2", model: authored }); + expect(translated).toMatchObject({ id: "deepseek/deepseek-v3.2", model: authored }); }); -test("Novita AI sync skips unknown remote models", () => { - expect(novitaAi.translateModel(novitaAiModel({ id: "novita/unknown-model" }), { +test("Novita AI sync creates unknown remote models from API metadata", () => { + const translated = novitaAi.translateModel(novitaAiModel({ id: "novita/unknown-model", context_size: 8192, max_output_tokens: 4096, pricing: { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } } }), { existing: () => undefined, authored: () => undefined, - })).toBeUndefined(); + }); + expect(translated?.id).toBe("novita/unknown-model"); + expect(translated?.model).toMatchObject({ limit: { context: 8192, output: 4096 }, cost: { input: 0.1, output: 0.2 } }); }); test("Novita AI sync retains local models absent from API response", async () => { From a43f09440356fa6aeb4e68652b53c8afa30ae52e Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 12:23:22 +0800 Subject: [PATCH 05/17] feat(sync): enable full Novita catalog lifecycle --- packages/core/src/sync/providers/novita-ai.ts | 2 +- packages/core/test/novita-ai.test.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 63c2b088688..badc64d4b28 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -125,7 +125,7 @@ export const novitaAi = { modelsDir: "providers/novita-ai/models", // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, - deleteMissing: false, + deleteMissing: true, sourceID(model) { return model.id; }, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 5af38165649..b1ba609e14b 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -98,7 +98,7 @@ test("Novita AI sync creates unknown remote models from API metadata", () => { expect(translated?.model).toMatchObject({ limit: { context: 8192, output: 4096 }, cost: { input: 0.1, output: 0.2 } }); }); -test("Novita AI sync retains local models absent from API response", async () => { +test("Novita AI sync removes local models absent from API response", async () => { const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-ai-")); const modelsDir = path.join(dir, "providers", "novita-ai", "models"); await mkdir(modelsDir, { recursive: true }); @@ -145,9 +145,8 @@ test("Novita AI sync retains local models absent from API response", async () => }; }, }); - expect(result.deleted).toBe(0); - expect(result.unchanged).toBe(1); - expect(await Bun.file(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml")).exists()).toBe(true); + expect(result.deleted).toBe(1); + expect(await Bun.file(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml")).exists()).toBe(false); } finally { await rm(dir, { recursive: true, force: true }); } From 5fbd74ad175c34339364be2c4477e7909609f061 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 15:43:36 +0800 Subject: [PATCH 06/17] fix(sync): avoid guessing Novita pricing and model capabilities --- packages/core/src/sync/providers/novita-ai.ts | 118 ++++++++++++------ packages/core/test/novita-ai.test.ts | 77 ++++++++++-- 2 files changed, 151 insertions(+), 44 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index badc64d4b28..76a6b4356d6 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -1,12 +1,17 @@ import { z } from "zod"; -import path from "node:path"; import { describeModel } from "../../describe.js"; -import { inferKimiFamily, ModelFamilyValues } from "../../family.js"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; +import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; const API_ENDPOINT = "https://api.novita.ai/openai/v1/models"; -const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models"); +const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); +const Pricing = z.object({ + prompt: Price.optional(), + completion: Price.optional(), + input_cache_read: Price.optional(), + input_cache_write: Price.optional(), +}).passthrough(); export const NovitaAIModel = z.object({ id: z.string().min(1), @@ -21,11 +26,13 @@ export const NovitaAIModel = z.object({ features: z.array(z.string()).optional(), input_modalities: z.array(z.string()).optional(), output_modalities: z.array(z.string()).optional(), - pricing: z.object({ - prompt: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), - completion: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), - input_cache_read: z.object({ price_per_m_decimal: z.string().optional() }).passthrough().optional(), - }).passthrough().optional(), + pricing: Pricing.optional(), + is_tiered_billing: z.boolean().optional(), + tiered_billing_configs: z.array(z.object({ + min_tokens: z.number().int().nonnegative(), + max_tokens: z.number().int().positive(), + pricing: Pricing, + }).passthrough()).optional(), }).passthrough(); export const NovitaAIResponse = z.object({ @@ -58,52 +65,84 @@ function dateFromTimestamp(timestamp: number) { return new Date(timestamp * 1000).toISOString().slice(0, 10); } -function inferFamily(id: string, name: string) { - const kimi = inferKimiFamily(id, name); - if (kimi !== undefined) return kimi; - const target = `${id} ${name}`.toLowerCase(); - return [...ModelFamilyValues].sort((a, b) => b.length - a.length).find((family) => - new RegExp(`(^|[^a-z0-9])${family.toLowerCase().replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}(?=$|[^a-z0-9])`).test(target)); +function price(pricing: z.infer | undefined) { + const input = decimalPrice(pricing?.prompt?.price_per_m_decimal); + const output = decimalPrice(pricing?.completion?.price_per_m_decimal); + if (input === undefined || output === undefined) return undefined; + return { + input, + output, + cache_read: decimalPrice(pricing?.input_cache_read?.price_per_m_decimal), + cache_write: decimalPrice(pricing?.input_cache_write?.price_per_m_decimal), + }; } -function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefined): SyncedModel { +function cost(model: NovitaAIModel, existing: ExistingModel | undefined) { + if (model.is_tiered_billing !== true) return price(model.pricing) ?? existing?.cost; + const bands = [...model.tiered_billing_configs ?? []].sort((a, b) => a.min_tokens - b.min_tokens); + if (bands.length === 0 || bands[0]?.min_tokens > 1 || bands.some((band, index) => + band.max_tokens <= band.min_tokens || (index > 0 && band.min_tokens <= bands[index - 1]!.min_tokens) + )) return existing?.cost; + const base = price(bands[0]!.pricing); + if (base === undefined || bands.some((band) => price(band.pricing) === undefined)) return existing?.cost; + return { + ...base, + tiers: bands.slice(1).map((band) => ({ + ...price(band.pricing)!, + tier: { type: "context" as const, size: band.min_tokens }, + })), + }; +} + +function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefined, resolved: ExistingModel | undefined): SyncedModel | undefined { + const baseModel = existing?.base_model ?? resolveModelMetadataBaseModel(model.id); + // New provider entries require a lab model. Do not create fabricated inline lab facts. + if (existing === undefined && baseModel === undefined) return undefined; const name = model.display_name ?? model.title ?? existing?.name ?? model.id; - const input = modalities(model.input_modalities, existing?.modalities?.input) ?? ["text"]; - const output = modalities(model.output_modalities, existing?.modalities?.output) ?? ["text"]; + const input = modalities(model.input_modalities, resolved?.modalities?.input) ?? ["text"]; + const output = modalities(model.output_modalities, resolved?.modalities?.output) ?? ["text"]; const features = model.features === undefined ? undefined : new Set(model.features); - const reasoning = features?.has("reasoning") ?? existing?.reasoning ?? false; - const toolCall = features?.has("function-calling") ?? existing?.tool_call ?? false; - const structuredOutput = features?.has("structured-outputs") ?? existing?.structured_output ?? false; - const context = model.context_size ?? existing?.limit?.context ?? 0; - const outputLimit = model.max_output_tokens ?? existing?.limit?.output ?? context; - const inputCost = decimalPrice(model.pricing?.prompt?.price_per_m_decimal); - const outputCost = decimalPrice(model.pricing?.completion?.price_per_m_decimal); - const cacheRead = decimalPrice(model.pricing?.input_cache_read?.price_per_m_decimal); - const cost = inputCost !== undefined && outputCost !== undefined - ? { input: inputCost, output: outputCost, cache_read: cacheRead } - : existing?.cost ?? { input: 0, output: 0 }; + const reasoning = features?.has("reasoning") ?? resolved?.reasoning ?? false; + const toolCall = features?.has("function-calling") ?? resolved?.tool_call ?? false; + const structuredOutput = features?.has("structured-outputs") ?? resolved?.structured_output ?? false; + const context = model.context_size ?? resolved?.limit?.context ?? 0; + const outputLimit = model.max_output_tokens ?? resolved?.limit?.output ?? context; + const modelCost = cost(model, existing); + // A missing price is unknown, not free. Neither can we infer API reasoning controls. + if (existing === undefined && (modelCost === undefined || reasoning)) return undefined; const values: SyncedFullModel = { name, - description: model.description ?? existing?.description ?? describeModel({ id: model.id, name, family: inferFamily(model.id, name), reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? true, limit: { context, output: outputLimit }, modalities: { input, output } }), - family: existing?.family ?? inferFamily(model.id, name), + description: model.description || existing?.description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), + family: existing?.family, release_date: existing?.release_date ?? dateFromTimestamp(model.created), last_updated: existing?.last_updated ?? dateFromTimestamp(model.created), attachment: input.some((value) => value !== "text"), reasoning, tool_call: toolCall, structured_output: structuredOutput, - temperature: existing?.temperature ?? true, - open_weights: existing?.open_weights ?? true, - cost, + temperature: existing?.temperature, + open_weights: existing?.open_weights ?? false, + cost: modelCost, limit: { context, output: outputLimit }, modalities: { input, output }, }; - if (existing?.base_model === undefined) return values; + if (baseModel !== undefined) return factorBaseModel(baseModel, { + ...values, + // These are lab facts, not claims made by the Novita catalog endpoint. + open_weights: existing?.open_weights, + release_date: existing?.release_date, + last_updated: existing?.last_updated, + temperature: existing?.temperature, + reasoning_options: existing?.reasoning_options, + interleaved: existing?.interleaved, + }, values.limit, existing?.base_model_omit); return { ...existing, ...values, - base_model: existing.base_model, - ...(existing.base_model_omit === undefined ? {} : { base_model_omit: existing.base_model_omit }), + reasoning_options: existing?.reasoning_options, + interleaved: existing?.interleaved, + status: existing?.status, + knowledge: existing?.knowledge, } as SyncedModel; } @@ -126,9 +165,13 @@ export const novitaAi = { // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, deleteMissing: true, + trackMissingModels: false, sourceID(model) { return model.id; }, + skippedNotice(ids) { + return ids.length === 0 ? [] : [`Novita models needing lab metadata, pricing, or verified reasoning controls: ${ids.join(", ")}`]; + }, async fetchModels() { const key = process.env.NOVITA_API_KEY; if (key === undefined) throw new Error("Novita AI sync requires NOVITA_API_KEY"); @@ -138,6 +181,7 @@ export const novitaAi = { return NovitaAIResponse.parse(raw).data; }, translateModel(model, context) { - return { id: model.id, model: buildNovitaModel(model, context.authored(model.id)) }; + const translated = buildNovitaModel(model, context.authored(model.id), context.existing(model.id)); + return translated === undefined ? undefined : { id: model.id, model: translated }; }, } satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index b1ba609e14b..3bd812c7d05 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -48,9 +48,9 @@ test("maps Novita catalog metadata onto existing models", () => { }, }), { existing: () => ({}), - authored: () => ({ base_model: "test/base", name: "Old", description: "Old", attachment: false, reasoning: false, tool_call: false, open_weights: true, limit: { context: 1, output: 1 }, modalities: { input: ["text"], output: ["text"] } }), + authored: () => ({ base_model: "deepseek/deepseek-v3.2", name: "Old", description: "Old", attachment: false, reasoning: false, tool_call: false, open_weights: true, limit: { context: 1, output: 1 }, modalities: { input: ["text"], output: ["text"] } }), }); - expect(translated?.model).toMatchObject({ name: "GLM 5.3 Flash", reasoning: true, tool_call: true, structured_output: true, limit: { context: 1_048_576, output: 131_072 }, cost: { input: 0.15, output: 0.5, cache_read: 0.03 }, modalities: { input: ["text", "image"], output: ["text"] } }); + expect(translated?.model).toMatchObject({ name: "GLM 5.3 Flash", limit: { context: 1_048_576, output: 131_072 }, cost: { input: 0.15, output: 0.5, cache_read: 0.03 }, modalities: { input: ["text", "image"] } }); }); test("rejects invalid Novita AI API responses", () => { @@ -86,16 +86,79 @@ test("Novita AI sync preserves authored metadata for existing models", () => { authored: () => authored, }); - expect(translated).toMatchObject({ id: "deepseek/deepseek-v3.2", model: authored }); + expect(translated).toMatchObject({ id: "deepseek/deepseek-v3.2", model: { + base_model: authored.base_model, + reasoning_options: authored.reasoning_options, + interleaved: authored.interleaved, + cost: authored.cost, + } }); }); -test("Novita AI sync creates unknown remote models from API metadata", () => { - const translated = novitaAi.translateModel(novitaAiModel({ id: "novita/unknown-model", context_size: 8192, max_output_tokens: 4096, pricing: { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } } }), { +test("Novita AI sync creates non-reasoning models with a known lab base and a price", () => { + const translated = novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v3", context_size: 8192, max_output_tokens: 4096, features: [], pricing: { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } } }), { existing: () => undefined, authored: () => undefined, }); - expect(translated?.id).toBe("novita/unknown-model"); - expect(translated?.model).toMatchObject({ limit: { context: 8192, output: 4096 }, cost: { input: 0.1, output: 0.2 } }); + expect(translated?.id).toBe("deepseek/deepseek-v3"); + expect(translated?.model).toMatchObject({ base_model: "deepseek/deepseek-v3", limit: { context: 8192, output: 4096 }, cost: { input: 0.1, output: 0.2 } }); +}); + +test("Novita AI sync skips new models with unknown lab, price, or reasoning controls", () => { + const context = { existing: () => undefined, authored: () => undefined }; + const price = { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } }; + expect(novitaAi.translateModel(novitaAiModel({ id: "novita/unknown-model", pricing: price }), context)).toBeUndefined(); + expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v3", features: [] }), context)).toBeUndefined(); + expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v3", features: ["reasoning"], pricing: price }), context)).toBeUndefined(); +}); + +test("Novita AI sync maps tiered context prices and cache-write", () => { + const pricing = (input: string, output: string, cacheWrite: string) => ({ + prompt: { price_per_m_decimal: input }, + completion: { price_per_m_decimal: output }, + input_cache_write: { price_per_m_decimal: cacheWrite }, + }); + const result = novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-v3", + features: [], + is_tiered_billing: true, + tiered_billing_configs: [ + { min_tokens: 256_000, max_tokens: 1_000_000, pricing: pricing("0.5", "3", "0.625") }, + { min_tokens: 1, max_tokens: 256_000, pricing: pricing("0.4", "2.4", "0.5") }, + ], + }), { existing: () => undefined, authored: () => undefined }); + expect(result?.model).toMatchObject({ cost: { + input: 0.4, output: 2.4, cache_write: 0.5, + tiers: [{ tier: { type: "context", size: 256_000 }, input: 0.5, output: 3, cache_write: 0.625 }], + } }); +}); + +test("Novita AI sync preserves inherited capabilities when features are absent", () => { + const authored = { base_model: "deepseek/deepseek-v3.2", cost: { input: 0.1, output: 0.2 } }; + const resolved = { ...authored, reasoning: true, tool_call: true, modalities: { input: ["text" as const], output: ["text" as const] } }; + const translated = novitaAi.translateModel(novitaAiModel(), { + authored: () => authored, + existing: () => resolved, + }); + expect(translated?.model).toMatchObject({ base_model: authored.base_model }); + expect(translated?.model).not.toHaveProperty("reasoning", false); + expect(translated?.model).not.toHaveProperty("tool_call", false); +}); + +test("Novita AI sync updates existing inline model capabilities", () => { + const existing = { + name: "Old", description: "Old", reasoning: false, tool_call: false, + attachment: false, open_weights: false, release_date: "2025-01-01", last_updated: "2025-01-01", + limit: { context: 8192, output: 4096 }, modalities: { input: ["text" as const], output: ["text" as const] }, + }; + const translated = novitaAi.translateModel(novitaAiModel({ + id: "novita/custom-model", + display_name: "Updated", features: ["reasoning", "function-calling"], + input_modalities: ["text", "image"], + }), { authored: () => existing, existing: () => existing }); + expect(translated?.model).toMatchObject({ + name: "Updated", reasoning: true, tool_call: true, attachment: true, + modalities: { input: ["text", "image"] }, + }); }); test("Novita AI sync removes local models absent from API response", async () => { From 89c2339a05dbb811470a3320ffda3273667f175a Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 16:09:31 +0800 Subject: [PATCH 07/17] fix(sync): recognize Novita free models and verified controls --- packages/core/src/sync/providers/novita-ai.ts | 22 +++++++--- packages/core/test/novita-ai.test.ts | 41 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 76a6b4356d6..59cb58e2966 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -5,6 +5,9 @@ import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from " import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; const API_ENDPOINT = "https://api.novita.ai/openai/v1/models"; +const BASE_MODEL_ALIASES: Record = { + "deepseek/deepseek_v3": "deepseek/deepseek-v3", +}; const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); const Pricing = z.object({ prompt: Price.optional(), @@ -18,6 +21,8 @@ export const NovitaAIModel = z.object({ object: z.literal("model"), created: z.number().int().nonnegative(), owned_by: z.string(), + input_token_price_per_m: z.number().optional(), + output_token_price_per_m: z.number().optional(), title: z.string().optional(), display_name: z.string().optional(), description: z.string().optional(), @@ -78,7 +83,13 @@ function price(pricing: z.infer | undefined) { } function cost(model: NovitaAIModel, existing: ExistingModel | undefined) { - if (model.is_tiered_billing !== true) return price(model.pricing) ?? existing?.cost; + if (model.is_tiered_billing !== true) { + // Novita uses zero top-level prices without a pricing object for free models. + if (model.pricing === undefined && model.input_token_price_per_m === 0 && model.output_token_price_per_m === 0) { + return { input: 0, output: 0 }; + } + return price(model.pricing) ?? existing?.cost; + } const bands = [...model.tiered_billing_configs ?? []].sort((a, b) => a.min_tokens - b.min_tokens); if (bands.length === 0 || bands[0]?.min_tokens > 1 || bands.some((band, index) => band.max_tokens <= band.min_tokens || (index > 0 && band.min_tokens <= bands[index - 1]!.min_tokens) @@ -95,7 +106,7 @@ function cost(model: NovitaAIModel, existing: ExistingModel | undefined) { } function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefined, resolved: ExistingModel | undefined): SyncedModel | undefined { - const baseModel = existing?.base_model ?? resolveModelMetadataBaseModel(model.id); + const baseModel = existing?.base_model ?? BASE_MODEL_ALIASES[model.id] ?? resolveModelMetadataBaseModel(model.id); // New provider entries require a lab model. Do not create fabricated inline lab facts. if (existing === undefined && baseModel === undefined) return undefined; const name = model.display_name ?? model.title ?? existing?.name ?? model.id; @@ -108,8 +119,9 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const context = model.context_size ?? resolved?.limit?.context ?? 0; const outputLimit = model.max_output_tokens ?? resolved?.limit?.output ?? context; const modelCost = cost(model, existing); - // A missing price is unknown, not free. Neither can we infer API reasoning controls. - if (existing === undefined && (modelCost === undefined || reasoning)) return undefined; + // DeepSeek R1 is fixed-reasoning on Novita, as with its already curated R1 variants. + const reasoningOptions = existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); + if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, description: model.description || existing?.description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), @@ -133,7 +145,7 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi release_date: existing?.release_date, last_updated: existing?.last_updated, temperature: existing?.temperature, - reasoning_options: existing?.reasoning_options, + reasoning_options: reasoningOptions, interleaved: existing?.interleaved, }, values.limit, existing?.base_model_omit); return { diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 3bd812c7d05..91be817447b 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -111,6 +111,47 @@ test("Novita AI sync skips new models with unknown lab, price, or reasoning cont expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v3", features: ["reasoning"], pricing: price }), context)).toBeUndefined(); }); +test("Novita AI sync treats explicit zero prices without tiers as free", () => { + const model = novitaAiModel({ + id: "inclusionai/ling-3.0-flash-fin", + input_token_price_per_m: 0, + output_token_price_per_m: 0, + features: ["reasoning"], + }); + expect(novitaAi.translateModel(model, { existing: () => undefined, authored: () => undefined })).toBeUndefined(); + const authored = { base_model: "inclusionai/ling-3.0-flash-fin", reasoning_options: [] }; + const translated = novitaAi.translateModel(model, { existing: () => authored, authored: () => authored }); + expect(translated?.model).toMatchObject({ + base_model: "inclusionai/ling-3.0-flash-fin", cost: { input: 0, output: 0 }, + }); +}); + +test("Novita AI sync does not mistake tier-only pricing for free", () => { + const translated = novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-v3", + input_token_price_per_m: 0, + output_token_price_per_m: 0, + is_tiered_billing: true, + features: [], + tiered_billing_configs: [{ + min_tokens: 1, max_tokens: 10_000, + pricing: { prompt: { price_per_m_decimal: "0.5" }, completion: { price_per_m_decimal: "2" } }, + }], + }), { existing: () => undefined, authored: () => undefined }); + expect(translated?.model).toMatchObject({ cost: { input: 0.5, output: 2 } }); +}); + +test("Novita AI sync reuses a verified lab alias and fixed R1 controls", () => { + const context = { existing: () => undefined, authored: () => undefined }; + const pricing = { prompt: { price_per_m_decimal: "0.89" }, completion: { price_per_m_decimal: "0.89" } }; + expect(novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek_v3", features: [], pricing, + }), context)?.model).toMatchObject({ base_model: "deepseek/deepseek-v3" }); + expect(novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-r1", features: ["reasoning"], pricing, + }), context)?.model).toMatchObject({ base_model: "deepseek/deepseek-r1", reasoning_options: [] }); +}); + test("Novita AI sync maps tiered context prices and cache-write", () => { const pricing = (input: string, output: string, cacheWrite: string) => ({ prompt: { price_per_m_decimal: input }, From ed34f5b29604f080e2dcd64d09ec7086f1cb7582 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 16:24:37 +0800 Subject: [PATCH 08/17] fix(sync): harden Novita catalog automation and preserve authored prices --- packages/core/src/sync/index.ts | 24 +++++ packages/core/src/sync/missing-issues.ts | 4 +- packages/core/src/sync/providers/novita-ai.ts | 33 ++++--- packages/core/test/novita-ai.test.ts | 95 ++++++++++++++++++- .../models/baichuan/baichuan-m2-32b.toml | 8 +- .../baidu/ernie-4.5-21B-a3b-thinking.toml | 24 ----- .../models/baidu/ernie-4.5-21B-a3b.toml | 3 +- .../baidu/ernie-4.5-300b-a47b-paddle.toml | 22 ----- .../baidu/ernie-4.5-vl-28b-a3b-thinking.toml | 23 ----- .../models/baidu/ernie-4.5-vl-28b-a3b.toml | 22 ----- .../models/baidu/ernie-4.5-vl-424b-a47b.toml | 3 +- .../models/deepseek/deepseek-ocr-2.toml | 17 +--- .../models/deepseek/deepseek-ocr.toml | 22 ----- .../deepseek/deepseek-prover-v2-671b.toml | 21 ---- .../deepseek/deepseek-r1-0528-qwen3-8b.toml | 5 +- .../deepseek-r1-distill-llama-70b.toml | 6 +- .../deepseek-r1-distill-qwen-14b.toml | 22 ----- .../deepseek-r1-distill-qwen-32b.toml | 22 ----- .../models/deepseek/deepseek-r1-turbo.toml | 5 +- .../models/deepseek/deepseek-r1.toml | 13 +++ .../models/deepseek/deepseek-v3-0324.toml | 25 ----- .../models/deepseek/deepseek-v3-turbo.toml | 21 ---- .../deepseek/deepseek-v3.1-terminus.toml | 4 +- .../models/deepseek/deepseek-v3.1.toml | 16 +--- .../models/deepseek/deepseek-v3.2-exp.toml | 2 +- .../models/deepseek/deepseek-v3.2.toml | 25 ++--- .../models/deepseek/deepseek-v4-flash.toml | 9 +- .../models/deepseek/deepseek-v4-pro.toml | 9 +- .../models/deepseek/deepseek_v3.toml | 12 +++ .../models/google/gemma-3-12b-it.toml | 12 +-- .../models/google/gemma-3-27b-it.toml | 11 +-- .../models/google/gemma-4-26b-a4b-it.toml | 19 +--- .../models/google/gemma-4-31b-it.toml | 19 +--- .../models/gryphe/mythomax-l2-13b.toml | 3 +- .../models/inclusionai/ling-2.6-1t.toml | 24 ----- .../models/inclusionai/ling-2.6-flash.toml | 24 ----- .../models/inclusionai/ring-2.6-1t.toml | 25 ----- .../models/kwaipilot/kat-coder-pro.toml | 23 ----- .../meta-llama/llama-3-70b-instruct.toml | 23 ----- .../meta-llama/llama-3-8b-instruct.toml | 22 ----- .../meta-llama/llama-3.1-8b-instruct.toml | 11 +-- .../meta-llama/llama-3.2-3b-instruct.toml | 22 ----- .../meta-llama/llama-3.3-70b-instruct.toml | 17 +--- ...lama-4-maverick-17b-128e-instruct-fp8.toml | 1 + .../llama-4-scout-17b-16e-instruct.toml | 1 + .../models/microsoft/wizardlm-2-8x22b.toml | 3 +- .../models/minimax/minimax-m2.1.toml | 24 +---- .../minimax/minimax-m2.5-highspeed.toml | 18 +--- .../models/minimax/minimax-m2.5.toml | 21 +--- .../minimax/minimax-m2.7-highspeed.toml | 4 +- .../models/minimax/minimax-m2.7.toml | 24 +---- .../novita-ai/models/minimax/minimax-m2.toml | 26 ++--- .../models/minimaxai/minimax-m1-80k.toml | 3 +- .../models/mistralai/mistral-nemo.toml | 11 +-- .../models/moonshotai/kimi-k2-0905.toml | 6 +- .../models/moonshotai/kimi-k2-instruct.toml | 3 +- .../models/moonshotai/kimi-k2-thinking.toml | 23 ++--- .../models/moonshotai/kimi-k2.5.toml | 29 ++---- .../models/moonshotai/kimi-k2.6.toml | 5 +- .../models/moonshotai/kimi-k2.7-code.toml | 10 +- .../novita-ai/models/moonshotai/kimi-k3.toml | 9 +- .../novita-ai/models/openai/gpt-oss-120b.toml | 16 +--- .../novita-ai/models/openai/gpt-oss-20b.toml | 17 ++-- .../models/paddlepaddle/paddleocr-vl.toml | 3 +- .../models/qwen/qwen-2.5-72b-instruct.toml | 4 +- .../novita-ai/models/qwen/qwen-mt-plus.toml | 1 + .../models/qwen/qwen2.5-7b-instruct.toml | 22 ----- .../models/qwen/qwen2.5-vl-72b-instruct.toml | 22 ----- .../models/qwen/qwen3-235b-a22b-fp8.toml | 3 +- .../qwen/qwen3-235b-a22b-instruct-2507.toml | 13 +-- .../qwen/qwen3-235b-a22b-thinking-2507.toml | 9 +- .../models/qwen/qwen3-30b-a3b-fp8.toml | 22 ----- .../novita-ai/models/qwen/qwen3-32b-fp8.toml | 22 ----- .../novita-ai/models/qwen/qwen3-4b-fp8.toml | 22 ----- .../novita-ai/models/qwen/qwen3-8b-fp8.toml | 22 ----- .../qwen/qwen3-coder-30b-a3b-instruct.toml | 12 +-- .../qwen/qwen3-coder-480b-a35b-instruct.toml | 16 +--- .../models/qwen/qwen3-coder-next.toml | 19 +--- .../novita-ai/models/qwen/qwen3-max.toml | 29 +++--- .../qwen/qwen3-next-80b-a3b-instruct.toml | 16 +--- .../qwen/qwen3-next-80b-a3b-thinking.toml | 23 ----- .../qwen/qwen3-vl-235b-a22b-instruct.toml | 13 +-- .../qwen/qwen3-vl-235b-a22b-thinking.toml | 13 +-- .../qwen/qwen3-vl-30b-a3b-instruct.toml | 2 +- .../qwen/qwen3-vl-30b-a3b-thinking.toml | 22 ----- .../models/qwen/qwen3-vl-8b-instruct.toml | 22 ----- .../models/qwen/qwen3.5-122b-a10b.toml | 19 +--- .../novita-ai/models/qwen/qwen3.5-27b.toml | 18 +--- .../models/qwen/qwen3.5-35b-a3b.toml | 21 ++-- .../models/qwen/qwen3.5-397b-a17b.toml | 19 +--- .../novita-ai/models/qwen/qwen3.7-max.toml | 21 +--- .../models/sao10K/L3-8B-stheno-v3.2.toml | 22 ----- .../models/sao10K/l3-70b-euryale-v2.1.toml | 21 ---- .../models/sao10K/l3-8b-lunaris.toml | 22 ----- .../models/sao10K/l31-70b-euryale-v2.2.toml | 21 ---- .../models/xiaomimimo/mimo-v2-flash.toml | 26 ----- .../models/xiaomimimo/mimo-v2-pro.toml | 18 ---- .../models/xiaomimimo/mimo-v2.5-pro.toml | 10 +- .../autoglm-phone-9b-multilingual.toml | 3 +- .../novita-ai/models/zai-org/glm-4.5-air.toml | 21 +--- .../novita-ai/models/zai-org/glm-4.5.toml | 29 ------ .../novita-ai/models/zai-org/glm-4.5v.toml | 19 +--- .../novita-ai/models/zai-org/glm-4.6.toml | 27 ++---- .../novita-ai/models/zai-org/glm-4.6v.toml | 18 +--- .../models/zai-org/glm-4.7-flash.toml | 21 +--- .../novita-ai/models/zai-org/glm-4.7.toml | 29 ++---- .../novita-ai/models/zai-org/glm-5.1.toml | 25 ++--- .../novita-ai/models/zai-org/glm-5.2.toml | 10 +- providers/novita-ai/models/zai-org/glm-5.toml | 27 ++---- sync.md | 7 ++ 110 files changed, 436 insertions(+), 1416 deletions(-) delete mode 100644 providers/novita-ai/models/baidu/ernie-4.5-21B-a3b-thinking.toml delete mode 100644 providers/novita-ai/models/baidu/ernie-4.5-300b-a47b-paddle.toml delete mode 100644 providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b-thinking.toml delete mode 100644 providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-ocr.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-prover-v2-671b.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-14b.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-32b.toml create mode 100644 providers/novita-ai/models/deepseek/deepseek-r1.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-v3-0324.toml delete mode 100644 providers/novita-ai/models/deepseek/deepseek-v3-turbo.toml create mode 100644 providers/novita-ai/models/deepseek/deepseek_v3.toml delete mode 100644 providers/novita-ai/models/inclusionai/ling-2.6-1t.toml delete mode 100644 providers/novita-ai/models/inclusionai/ling-2.6-flash.toml delete mode 100644 providers/novita-ai/models/inclusionai/ring-2.6-1t.toml delete mode 100644 providers/novita-ai/models/kwaipilot/kat-coder-pro.toml delete mode 100644 providers/novita-ai/models/meta-llama/llama-3-70b-instruct.toml delete mode 100644 providers/novita-ai/models/meta-llama/llama-3-8b-instruct.toml delete mode 100644 providers/novita-ai/models/meta-llama/llama-3.2-3b-instruct.toml delete mode 100644 providers/novita-ai/models/qwen/qwen2.5-7b-instruct.toml delete mode 100644 providers/novita-ai/models/qwen/qwen2.5-vl-72b-instruct.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-30b-a3b-fp8.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-32b-fp8.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-4b-fp8.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-8b-fp8.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-next-80b-a3b-thinking.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-thinking.toml delete mode 100644 providers/novita-ai/models/qwen/qwen3-vl-8b-instruct.toml delete mode 100644 providers/novita-ai/models/sao10K/L3-8B-stheno-v3.2.toml delete mode 100644 providers/novita-ai/models/sao10K/l3-70b-euryale-v2.1.toml delete mode 100644 providers/novita-ai/models/sao10K/l3-8b-lunaris.toml delete mode 100644 providers/novita-ai/models/sao10K/l31-70b-euryale-v2.2.toml delete mode 100644 providers/novita-ai/models/xiaomimimo/mimo-v2-flash.toml delete mode 100644 providers/novita-ai/models/xiaomimimo/mimo-v2-pro.toml delete mode 100644 providers/novita-ai/models/zai-org/glm-4.5.toml diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 1d06bf0b10f..0b68beb478c 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -84,6 +84,8 @@ export interface SyncProvider { skipCreates?: boolean; /** Report remote-only models skipped by skipCreates as GitHub issues. */ trackMissingModels?: boolean; + /** Maximum share of existing files that may disappear in one sync. */ + maxMissingFraction?: number; deleteMissing?: boolean; preserveSymlinks?: boolean; preserveBaseModels?: boolean; @@ -97,6 +99,8 @@ export interface SyncProvider { * undefined to skip silently (no notice, no missing-model issue). */ sourceID?(model: SourceModel): string | undefined; + /** Track an untranslatable remote model without deleting an existing local entry. */ + missingModelID?(model: SourceModel): string | undefined; skippedNotice?(ids: string[]): string[]; fetchModels(): Promise; parseModels(raw: unknown): SourceModel[]; @@ -259,6 +263,7 @@ export async function syncProvider( const caseNormalizedDesiredPaths = new Map(); const desiredMetadata = new Map; content: string }>(); const skippedRemote: string[] = []; + const missingRemote = new Set(); const missingReasoning = new Map(); for (const sourceModel of sourceModels) { @@ -281,6 +286,8 @@ export async function syncProvider( if (translated === undefined) { const skippedID = provider.sourceID?.(sourceModel); if (skippedID !== undefined) skippedRemote.push(skippedID); + const missingID = provider.missingModelID?.(sourceModel); + if (missingID !== undefined) missingRemote.add(missingID); continue; } @@ -367,6 +374,18 @@ export async function syncProvider( }); } + if (provider.deleteMissing !== false && provider.maxMissingFraction !== undefined) { + if (provider.maxMissingFraction < 0 || provider.maxMissingFraction > 1) { + throw new Error(`Invalid maxMissingFraction for ${provider.id}`); + } + const absent = [...existing.keys()].filter((file) => + !desired.has(file) && !missingRemote.has(file.slice(0, -5)) && !missingReasoning.has(file.slice(0, -5)) + ).length; + if (existing.size > 0 && absent / existing.size > provider.maxMissingFraction) { + throw new Error(`${provider.id} sync would delete ${absent}/${existing.size} existing models; refusing unusually large catalog shrink`); + } + } + const files: SyncResult["files"] = []; let unchanged = 0; @@ -460,6 +479,10 @@ export async function syncProvider( const missingLocal: string[] = []; for (const relativePath of new Set([...existing.keys(), ...brokenSymlinks])) { if (desired.has(relativePath)) continue; + if (missingRemote.has(relativePath.slice(0, -5))) { + unchanged++; + continue; + } if (missingReasoning.has(relativePath.slice(0, -5))) { unchanged++; continue; @@ -492,6 +515,7 @@ export async function syncProvider( ]; const issueModels = [ + ...missingRemote, ...(provider.skipCreates === true ? skippedRemote : []), ...missingReasoning.keys(), ]; diff --git a/packages/core/src/sync/missing-issues.ts b/packages/core/src/sync/missing-issues.ts index 81a01a3742e..fb2edaf667c 100644 --- a/packages/core/src/sync/missing-issues.ts +++ b/packages/core/src/sync/missing-issues.ts @@ -16,7 +16,7 @@ function issueTitle(providerId: string, modelId: string) { function issueBody(provider: MissingModelIssueTarget, modelId: string, reason?: string) { return [ reason === undefined - ? `The **${provider.name}** catalog sync found remote model \`${modelId}\` that is not in the local catalog.` + ? `The **${provider.name}** catalog sync cannot automatically translate remote model \`${modelId}\`. Any existing local entry was left unchanged.` : `The **${provider.name}** catalog sync is missing reasoning options for remote model \`${modelId}\`. Any existing local entry was left unchanged.`, "", `| Field | Value |`, @@ -26,7 +26,7 @@ function issueBody(provider: MissingModelIssueTarget, modelId: string, reason?: `| Expected path | \`${provider.modelsDir}/${modelId}.toml\` |`, "", reason === undefined - ? "This provider uses `skipCreates` because the remote source is not enough to auto-author a full TOML." + ? "The remote source does not provide enough verified metadata to auto-author a full TOML." : `Sync diagnostic: ${reason}`, "Add the model manually (prefer `base_model` when matching `models/` metadata exists).", ...(reason === undefined ? [] : [ diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 59cb58e2966..64854cda5b5 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -44,7 +44,7 @@ export const NovitaAIResponse = z.object({ // Novita's endpoint currently omits the OpenAI-compatible top-level object. // Keep accepting the standard value if the API adds it later. object: z.literal("list").optional(), - data: z.array(NovitaAIModel), + data: z.array(NovitaAIModel).min(1), }).passthrough(); export type NovitaAIModel = z.infer; @@ -70,15 +70,20 @@ function dateFromTimestamp(timestamp: number) { return new Date(timestamp * 1000).toISOString().slice(0, 10); } -function price(pricing: z.infer | undefined) { +type Cost = NonNullable; + +function price(pricing: z.infer | undefined, existing?: Cost) { const input = decimalPrice(pricing?.prompt?.price_per_m_decimal); const output = decimalPrice(pricing?.completion?.price_per_m_decimal); if (input === undefined || output === undefined) return undefined; return { input, output, - cache_read: decimalPrice(pricing?.input_cache_read?.price_per_m_decimal), - cache_write: decimalPrice(pricing?.input_cache_write?.price_per_m_decimal), + reasoning: existing?.reasoning, + cache_read: decimalPrice(pricing?.input_cache_read?.price_per_m_decimal) ?? existing?.cache_read, + cache_write: decimalPrice(pricing?.input_cache_write?.price_per_m_decimal) ?? existing?.cache_write, + input_audio: existing?.input_audio, + output_audio: existing?.output_audio, }; } @@ -86,20 +91,20 @@ function cost(model: NovitaAIModel, existing: ExistingModel | undefined) { if (model.is_tiered_billing !== true) { // Novita uses zero top-level prices without a pricing object for free models. if (model.pricing === undefined && model.input_token_price_per_m === 0 && model.output_token_price_per_m === 0) { - return { input: 0, output: 0 }; + return { ...existing?.cost, input: 0, output: 0, tiers: undefined }; } - return price(model.pricing) ?? existing?.cost; + return price(model.pricing, existing?.cost) ?? existing?.cost; } const bands = [...model.tiered_billing_configs ?? []].sort((a, b) => a.min_tokens - b.min_tokens); - if (bands.length === 0 || bands[0]?.min_tokens > 1 || bands.some((band, index) => + if (bands.length === 0 || bands[0]!.min_tokens > 1 || bands.some((band, index) => band.max_tokens <= band.min_tokens || (index > 0 && band.min_tokens <= bands[index - 1]!.min_tokens) )) return existing?.cost; - const base = price(bands[0]!.pricing); + const base = price(bands[0]!.pricing, existing?.cost); if (base === undefined || bands.some((band) => price(band.pricing) === undefined)) return existing?.cost; return { ...base, tiers: bands.slice(1).map((band) => ({ - ...price(band.pricing)!, + ...price(band.pricing, existing?.cost?.tiers?.find((tier) => tier.tier.size === band.min_tokens))!, tier: { type: "context" as const, size: band.min_tokens }, })), }; @@ -124,7 +129,7 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, - description: model.description || existing?.description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), + description: existing?.description || model.description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), family: existing?.family, release_date: existing?.release_date ?? dateFromTimestamp(model.created), last_updated: existing?.last_updated ?? dateFromTimestamp(model.created), @@ -158,7 +163,7 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi } as SyncedModel; } -export async function fetchNovitaAIModels(key: string, fetcher: typeof fetch = fetch) { +export async function fetchNovitaAIModels(key: string, fetcher: (url: string, init?: RequestInit) => Promise = fetch) { const response = await fetcher(API_ENDPOINT, { method: "GET", headers: { Authorization: `Bearer ${key}` }, @@ -177,7 +182,11 @@ export const novitaAi = { // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, deleteMissing: true, - trackMissingModels: false, + trackMissingModels: true, + maxMissingFraction: 0.5, + missingModelID(model) { + return model.id; + }, sourceID(model) { return model.id; }, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 91be817447b..829166f4f95 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { groups, providers, syncProvider } from "../src/sync/index.js"; +import { groups, providers, syncProvider, type ExistingModel } from "../src/sync/index.js"; import { fetchNovitaAIModels, NovitaAIResponse, novitaAi, type NovitaAIModel } from "../src/sync/providers/novita-ai.js"; function novitaAiModel(overrides: Partial = {}): NovitaAIModel { @@ -54,14 +54,57 @@ test("maps Novita catalog metadata onto existing models", () => { }); test("rejects invalid Novita AI API responses", () => { + expect(() => NovitaAIResponse.parse({ data: [] })).toThrow(); expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "bad", object: "not-model", created: 1, owned_by: "" }] })) .toThrow(); expect(() => NovitaAIResponse.parse({ object: "list", data: [{ id: "", object: "model", created: -1, owned_by: "" }] })) .toThrow(); }); +test("Novita AI sync retains prices absent from the API", () => { + const existing = { + base_model: "deepseek/deepseek-v3", + cost: { input: 1, output: 2, cache_read: 0.2, cache_write: 1.5625, input_audio: 2.2, output_audio: 1.788, reasoning: 0.4 }, + }; + const result = novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-v3", features: [], + pricing: { prompt: { price_per_m_decimal: "0.7" }, completion: { price_per_m_decimal: "1.5" }, input_cache_read: { price_per_m_decimal: "0.1" } }, + }), { authored: () => existing, existing: () => existing }); + expect(result?.model.cost).toMatchObject({ + input: 0.7, output: 1.5, cache_read: 0.1, cache_write: 1.5625, + input_audio: 2.2, output_audio: 1.788, reasoning: 0.4, + }); +}); + +test("Novita AI sync preserves optional tier prices only at matching thresholds", () => { + const existing = { + base_model: "deepseek/deepseek-v3", + cost: { + input: 1, output: 2, cache_write: 0.3, input_audio: 2.2, + tiers: [{ tier: { type: "context" as const, size: 256_000 }, input: 3, output: 4, cache_write: 0.7 }, + { tier: { type: "context" as const, size: 500_000 }, input: 5, output: 6, cache_write: 0.9 }], + }, + }; + const result = novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-v3", features: [], is_tiered_billing: true, + tiered_billing_configs: [ + { min_tokens: 1, max_tokens: 256_000, pricing: { prompt: { price_per_m_decimal: "1.5" }, completion: { price_per_m_decimal: "2.5" } } }, + { min_tokens: 256_000, max_tokens: 750_000, pricing: { prompt: { price_per_m_decimal: "3.5" }, completion: { price_per_m_decimal: "4.5" } } }, + { min_tokens: 750_000, max_tokens: 1_000_000, pricing: { prompt: { price_per_m_decimal: "5.5" }, completion: { price_per_m_decimal: "6.5" } } }, + ], + }), { authored: () => existing, existing: () => existing }); + expect(result?.model.cost).toMatchObject({ + input: 1.5, output: 2.5, cache_write: 0.3, input_audio: 2.2, + tiers: [ + { tier: { size: 256_000 }, input: 3.5, output: 4.5, cache_write: 0.7 }, + { tier: { size: 750_000 }, input: 5.5, output: 6.5 }, + ], + }); + expect(result?.model.cost?.tiers?.[1]?.cache_write).toBeUndefined(); +}); + test("Novita AI sync preserves authored metadata for existing models", () => { - const authored = { + const authored: ExistingModel = { base_model: "deepseek/deepseek-v3.2", name: "Deepseek V3.2", description: "DeepSeek chat model for instruction following, coding, and analysis", @@ -124,6 +167,7 @@ test("Novita AI sync treats explicit zero prices without tiers as free", () => { expect(translated?.model).toMatchObject({ base_model: "inclusionai/ling-3.0-flash-fin", cost: { input: 0, output: 0 }, }); + expect(translated?.model.cost).not.toHaveProperty("base_model"); }); test("Novita AI sync does not mistake tier-only pricing for free", () => { @@ -242,6 +286,7 @@ test("Novita AI sync removes local models absent from API response", async () => const result = await syncProvider({ ...novitaAi, modelsDir, + maxMissingFraction: 1, async fetchModels() { return { object: "list", @@ -256,11 +301,57 @@ test("Novita AI sync removes local models absent from API response", async () => } }); +test("Novita AI sync refuses a partial response before updating any files", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-guard-")); + const modelsDir = path.join(dir, "providers", "novita-ai", "models"); + const file = path.join(modelsDir, "novita", "custom.toml"); + const other = path.join(modelsDir, "novita", "other.toml"); + const content = 'name = "Custom"\ndescription = "Custom hosted model"\nrelease_date = "2025-01-01"\nlast_updated = "2025-01-01"\nattachment = false\nreasoning = false\ntool_call = false\nopen_weights = false\n\n[cost]\ninput = 1\noutput = 2\n\n[limit]\ncontext = 8192\noutput = 4096\n\n[modalities]\ninput = ["text"]\noutput = ["text"]\n'; + try { + await mkdir(path.dirname(file), { recursive: true }); + await mkdir(path.dirname(other), { recursive: true }); + await Bun.write(file, content); + await Bun.write(other, content); + await expect(syncProvider({ + ...novitaAi, modelsDir, maxMissingFraction: 0.49, + async fetchModels() { return { data: [novitaAiModel({ id: "novita/custom", features: [], pricing: { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } } })] }; }, + })).rejects.toThrow("would delete 1/2 existing models"); + expect(await Bun.file(file).text()).toBe(content); + expect(await Bun.file(other).text()).toBe(content); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("Novita AI sync keeps local files when translation skips an existing remote ID", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-skip-")); + const modelsDir = path.join(dir, "providers", "novita-ai", "models"); + const file = path.join(modelsDir, "novita", "custom.toml"); + const content = 'name = "Custom"\ndescription = "Custom hosted model"\nrelease_date = "2025-01-01"\nlast_updated = "2025-01-01"\nattachment = false\nreasoning = false\ntool_call = false\nopen_weights = false\n\n[cost]\ninput = 1\noutput = 2\n\n[limit]\ncontext = 8192\noutput = 4096\n\n[modalities]\ninput = ["text"]\noutput = ["text"]\n'; + try { + await mkdir(path.dirname(file), { recursive: true }); + await Bun.write(file, content); + const result = await syncProvider({ + ...novitaAi, modelsDir, + async fetchModels() { return { data: [novitaAiModel({ id: "novita/custom" })] }; }, + translateModel() { return undefined; }, + }, { dryRun: true, openIssues: true }); + expect(result.deleted).toBe(0); + expect(result.notices.join(" ")).toContain("novita/custom"); + expect(result.notices.join(" ")).toContain("Would open GitHub issue"); + expect(await Bun.file(file).text()).toBe(content); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("Novita AI sync tracks remote-only IDs", () => { expect(providers["novita-ai"]).toBe(novitaAi); expect(groups.aggregators).toContain("novita-ai"); expect(novitaAi.sourceID?.(novitaAiModel())).toBe("deepseek/deepseek-v3.2"); expect(novitaAi.sourceID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); + expect(novitaAi.trackMissingModels).toBe(true); + expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); }); test("Novita AI sync requires NOVITA_API_KEY", async () => { diff --git a/providers/novita-ai/models/baichuan/baichuan-m2-32b.toml b/providers/novita-ai/models/baichuan/baichuan-m2-32b.toml index 61ada86fca5..035d44bcc75 100644 --- a/providers/novita-ai/models/baichuan/baichuan-m2-32b.toml +++ b/providers/novita-ai/models/baichuan/baichuan-m2-32b.toml @@ -1,4 +1,4 @@ -name = "baichuan-m2-32b" +name = "Baichuan M2 32B" description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" family = "baichuan" release_date = "2025-08-13" @@ -6,9 +6,9 @@ last_updated = "2025-08-13" attachment = false reasoning = false temperature = true -knowledge = "2024-12" tool_call = false structured_output = false +knowledge = "2024-12" open_weights = true [cost] @@ -16,8 +16,8 @@ input = 0.07 output = 0.07 [limit] -context = 131072 -output = 131072 +context = 131_072 +output = 131_072 [modalities] input = ["text"] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b-thinking.toml b/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b-thinking.toml deleted file mode 100644 index d8969fa9f63..00000000000 --- a/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b-thinking.toml +++ /dev/null @@ -1,24 +0,0 @@ -name = "ERNIE-4.5-21B-A3B-Thinking" -description = "Reasoning model for deliberate analysis, multi-step problem solving, and tool use" -family = "ernie" -release_date = "2025-09-19" -last_updated = "2025-09-19" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -knowledge = "2025-03" -tool_call = false -open_weights = true - -[cost] -input = 0.07 -output = 0.28 - -[limit] -context = 131_072 -output = 65_536 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b.toml b/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b.toml index 4329808b555..d8cd5706523 100644 --- a/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b.toml +++ b/providers/novita-ai/models/baidu/ernie-4.5-21B-a3b.toml @@ -6,8 +6,9 @@ last_updated = "2025-06-30" attachment = false reasoning = false temperature = true -knowledge = "2025-03" tool_call = true +structured_output = false +knowledge = "2025-03" open_weights = true [cost] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-300b-a47b-paddle.toml b/providers/novita-ai/models/baidu/ernie-4.5-300b-a47b-paddle.toml deleted file mode 100644 index b53b21ad54a..00000000000 --- a/providers/novita-ai/models/baidu/ernie-4.5-300b-a47b-paddle.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "ERNIE 4.5 300B A47B" -description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" -release_date = "2025-06-30" -last_updated = "2025-06-30" -attachment = false -reasoning = false -temperature = true -tool_call = false -structured_output = true -open_weights = true - -[cost] -input = 0.28 -output = 1.1 - -[limit] -context = 123_000 -output = 12_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b-thinking.toml b/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b-thinking.toml deleted file mode 100644 index 15b24e7dc0e..00000000000 --- a/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b-thinking.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "ERNIE-4.5-VL-28B-A3B-Thinking" -description = "Multimodal reasoning model for visual analysis, planning, and tool use" -release_date = "2025-11-26" -last_updated = "2025-11-26" -attachment = true -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.39 -output = 0.39 - -[limit] -context = 131_072 -output = 65_536 - -[modalities] -input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b.toml b/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b.toml deleted file mode 100644 index 6a9968ee3f2..00000000000 --- a/providers/novita-ai/models/baidu/ernie-4.5-vl-28b-a3b.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "ERNIE 4.5 VL 28B A3B" -description = "Multimodal reasoning model for visual analysis, planning, and tool use" -release_date = "2025-06-30" -last_updated = "2026-06-14" -attachment = true -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true -open_weights = true - -[cost] -input = 0.14 -output = 0.56 - -[limit] -context = 30_000 -output = 8_000 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/baidu/ernie-4.5-vl-424b-a47b.toml b/providers/novita-ai/models/baidu/ernie-4.5-vl-424b-a47b.toml index 2f7125b0bcc..50dbaeab66d 100644 --- a/providers/novita-ai/models/baidu/ernie-4.5-vl-424b-a47b.toml +++ b/providers/novita-ai/models/baidu/ernie-4.5-vl-424b-a47b.toml @@ -4,10 +4,11 @@ release_date = "2025-06-30" last_updated = "2025-06-30" attachment = true reasoning = true -reasoning_options = [] temperature = true tool_call = false +structured_output = false open_weights = true +reasoning_options = [] [cost] input = 0.42 diff --git a/providers/novita-ai/models/deepseek/deepseek-ocr-2.toml b/providers/novita-ai/models/deepseek/deepseek-ocr-2.toml index c687f5c2da1..5496066329c 100644 --- a/providers/novita-ai/models/deepseek/deepseek-ocr-2.toml +++ b/providers/novita-ai/models/deepseek/deepseek-ocr-2.toml @@ -1,20 +1,7 @@ -name = "deepseek/deepseek-ocr-2" +base_model = "deepseek/deepseek-ocr-2" description = "OCR model for extracting structured text from documents and screenshots" -release_date = "2026-01-27" -last_updated = "2026-01-27" -attachment = true -reasoning = false -tool_call = false -open_weights = true +structured_output = false [cost] input = 0.03 output = 0.03 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-ocr.toml b/providers/novita-ai/models/deepseek/deepseek-ocr.toml deleted file mode 100644 index 56f6fc709bf..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-ocr.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "DeepSeek-OCR" -description = "OCR model for extracting structured text from documents and screenshots" -release_date = "2025-10-24" -last_updated = "2025-10-24" -attachment = true -reasoning = false -temperature = true -tool_call = false -structured_output = true -open_weights = true - -[cost] -input = 0.03 -output = 0.03 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-prover-v2-671b.toml b/providers/novita-ai/models/deepseek/deepseek-prover-v2-671b.toml deleted file mode 100644 index 06aac3d3e1f..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-prover-v2-671b.toml +++ /dev/null @@ -1,21 +0,0 @@ -name = "Deepseek Prover V2 671B" -description = "Flagship DeepSeek model for coding, reasoning, and agentic work" -release_date = "2025-04-30" -last_updated = "2025-04-30" -attachment = false -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.7 -output = 2.5 - -[limit] -context = 160_000 -output = 160_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-r1-0528-qwen3-8b.toml b/providers/novita-ai/models/deepseek/deepseek-r1-0528-qwen3-8b.toml index a3c7e763158..4e1a867cc5b 100644 --- a/providers/novita-ai/models/deepseek/deepseek-r1-0528-qwen3-8b.toml +++ b/providers/novita-ai/models/deepseek/deepseek-r1-0528-qwen3-8b.toml @@ -1,13 +1,14 @@ -name = "DeepSeek R1 0528 Qwen3 8B" +name = "DeepSeek R1 Distill Qwen3 8B 0528" description = "DeepSeek reasoning model for multi-step analysis, math, coding, and tools" release_date = "2025-05-29" last_updated = "2025-05-29" attachment = false reasoning = true -reasoning_options = [] temperature = true tool_call = false +structured_output = false open_weights = true +reasoning_options = [] [cost] input = 0.06 diff --git a/providers/novita-ai/models/deepseek/deepseek-r1-distill-llama-70b.toml b/providers/novita-ai/models/deepseek/deepseek-r1-distill-llama-70b.toml index 500a14e21e9..cb329952271 100644 --- a/providers/novita-ai/models/deepseek/deepseek-r1-distill-llama-70b.toml +++ b/providers/novita-ai/models/deepseek/deepseek-r1-distill-llama-70b.toml @@ -1,15 +1,15 @@ -name = "DeepSeek R1 Distill LLama 70B" +name = "DeepSeek R1 Distill Llama 70B" description = "DeepSeek reasoning model for multi-step analysis, math, coding, and tools" family = "deepseek-thinking" release_date = "2025-01-27" last_updated = "2025-01-27" attachment = false reasoning = true -reasoning_options = [] temperature = true tool_call = false -structured_output = true +structured_output = false open_weights = true +reasoning_options = [] [cost] input = 0.8 diff --git a/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-14b.toml b/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-14b.toml deleted file mode 100644 index a05d9cbaa66..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-14b.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "DeepSeek R1 Distill Qwen 14B" -description = "Qwen reasoning model for deliberate problem solving, math, and coding" -family = "deepseek-thinking" -release_date = "2025-01-20" -last_updated = "2025-01-20" -attachment = false -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.15 -output = 0.15 - -[limit] -context = 32_768 -output = 16_384 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-32b.toml b/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-32b.toml deleted file mode 100644 index 3b78039a9dd..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-r1-distill-qwen-32b.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "DeepSeek R1 Distill Qwen 32B" -description = "Qwen reasoning model for deliberate problem solving, math, and coding" -family = "deepseek-thinking" -release_date = "2025-01-20" -last_updated = "2025-01-20" -attachment = false -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.3 -output = 0.3 - -[limit] -context = 64_000 -output = 32_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-r1-turbo.toml b/providers/novita-ai/models/deepseek/deepseek-r1-turbo.toml index 23da112f12b..676b30dc6e5 100644 --- a/providers/novita-ai/models/deepseek/deepseek-r1-turbo.toml +++ b/providers/novita-ai/models/deepseek/deepseek-r1-turbo.toml @@ -1,13 +1,14 @@ -name = "DeepSeek R1 (Turbo) " +name = "DeepSeek R1 Turbo" description = "DeepSeek reasoning model for multi-step analysis, math, coding, and tools" release_date = "2025-03-05" last_updated = "2025-03-05" attachment = false reasoning = true -reasoning_options = [] temperature = true tool_call = true +structured_output = true open_weights = true +reasoning_options = [] [cost] input = 0.7 diff --git a/providers/novita-ai/models/deepseek/deepseek-r1.toml b/providers/novita-ai/models/deepseek/deepseek-r1.toml new file mode 100644 index 00000000000..fa92dcd7c96 --- /dev/null +++ b/providers/novita-ai/models/deepseek/deepseek-r1.toml @@ -0,0 +1,13 @@ +base_model = "deepseek/deepseek-r1" +name = "DeepSeek R1" +description = "DeepSeek R1 is the latest open-source model released by the DeepSeek team, featuring impressive reasoning capabilities, particularly achieving performance comparable to OpenAI's o1 model in mathematics, coding, and reasoning tasks." +structured_output = false +reasoning_options = [] + +[cost] +input = 4 +output = 4 + +[limit] +context = 64_000 +output = 16_000 diff --git a/providers/novita-ai/models/deepseek/deepseek-v3-0324.toml b/providers/novita-ai/models/deepseek/deepseek-v3-0324.toml deleted file mode 100644 index 4a4c1961851..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-v3-0324.toml +++ /dev/null @@ -1,25 +0,0 @@ -name = "DeepSeek V3 0324" -description = "DeepSeek chat model for instruction following, coding, and analysis" -family = "deepseek" -release_date = "2025-03-25" -last_updated = "2025-03-25" -attachment = false -reasoning = false -temperature = true -knowledge = "2024-07" -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.27 -output = 1.12 -cache_read = 0.135 - -[limit] -context = 163_840 -output = 163_840 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-v3-turbo.toml b/providers/novita-ai/models/deepseek/deepseek-v3-turbo.toml deleted file mode 100644 index 951e07096ee..00000000000 --- a/providers/novita-ai/models/deepseek/deepseek-v3-turbo.toml +++ /dev/null @@ -1,21 +0,0 @@ -name = "DeepSeek V3 (Turbo) " -description = "Fast DeepSeek model for efficient chat, coding help, and agent loops" -release_date = "2025-03-05" -last_updated = "2025-03-05" -attachment = false -reasoning = false -temperature = true -tool_call = true -open_weights = true - -[cost] -input = 0.4 -output = 1.3 - -[limit] -context = 64_000 -output = 16_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml b/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml index f12185cccab..b1c67cb6579 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml @@ -1,4 +1,4 @@ -name = "Deepseek V3.1 Terminus" +name = "DeepSeek V3.1 Terminus" description = "DeepSeek chat model for instruction following, coding, and analysis" family = "deepseek" release_date = "2025-09-22" @@ -15,7 +15,7 @@ type = "toggle" [cost] input = 0.27 -output = 1.0 +output = 1 cache_read = 0.135 [limit] diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.1.toml b/providers/novita-ai/models/deepseek/deepseek-v3.1.toml index 68208d89a44..9af285358a0 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.1.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.1.toml @@ -1,27 +1,15 @@ +base_model = "deepseek/deepseek-v3.1" name = "DeepSeek V3.1" description = "DeepSeek chat model for instruction following, coding, and analysis" -family = "deepseek" -release_date = "2025-08-21" -last_updated = "2025-08-21" -attachment = false -reasoning = true -temperature = true -tool_call = true structured_output = true -open_weights = true [[reasoning_options]] type = "toggle" [cost] input = 0.27 -output = 1.0 +output = 1 cache_read = 0.135 [limit] -context = 131_072 output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml index 7f4e845ee86..6922812888c 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml @@ -1,4 +1,4 @@ -name = "Deepseek V3.2 Exp" +name = "DeepSeek V3.2 Exp" description = "DeepSeek chat model for instruction following, coding, and analysis" release_date = "2025-09-29" last_updated = "2025-09-29" diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.2.toml b/providers/novita-ai/models/deepseek/deepseek-v3.2.toml index 88862eac490..6abf57fe73f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.2.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.2.toml @@ -1,15 +1,11 @@ -name = "Deepseek V3.2" +base_model = "deepseek/deepseek-v3.2" description = "DeepSeek chat model for instruction following, coding, and analysis" -family = "deepseek" -release_date = "2025-12-01" -last_updated = "2025-12-01" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.269 @@ -19,10 +15,3 @@ cache_read = 0.1345 [limit] context = 163_840 output = 65_536 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml index 09253a5a834..4b4f07e0ff5 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml @@ -1,9 +1,16 @@ base_model = "deepseek/deepseek-v4-flash" -reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["minimal", "low", "medium", "high", "xhigh"] }] +description = "DeepSeek-V4-Flash is a lightweight model meticulously designed by DeepSeek to deliver the ultimate combination of lightning-fast response times and unmatched cost-effectiveness. Engineered with fewer parameters and significantly lower activation overhead, V4-Flash provides an exceptionally fast and economical API service. At its core, V4-Flash demonstrates outstanding reasoning capabilities that closely rival the V4-Pro model. While featuring a slightly streamlined repository of world knowledge, it remains highly capable of satisfying the demands of most application scenarios. In Agentic applications, V4-Flash performs on par with the Pro version when handling standard and fundamental tasks. As the premier choice for developers prioritizing high concurrency, low latency, and cost efficiency, DeepSeek-V4-Flash serves as the optimal solution for deploying large-scale, high-frequency, and lightweight AI workloads." [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high", "xhigh"] + [cost] input = 0.14 output = 0.28 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml index eb84d4bdfd5..7ff7b56005d 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml @@ -1,9 +1,16 @@ base_model = "deepseek/deepseek-v4-pro" -reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "xhigh"] }] +description = "DeepSeek-V4-Pro is the next-generation flagship open-source large language model developed by DeepSeek, delivering comprehensive performance that rivals the world's premier closed-source models. Compared to its predecessor, V4-Pro achieves a breakthrough evolution in Agentic capabilities. It firmly holds the top position among open-source models in Agentic Coding, providing a high-quality, end-to-end code delivery experience that surpasses mainstream industry benchmarks (such as Sonnet 4.5). Furthermore, the model not only boasts an expansive repository of world knowledge that leads the open-source community, but it also demonstrates ultimate logical reasoning prowess in highly demanding evaluations—including mathematics, STEM, and competitive programming. In these rigorous domains, V4-Pro outperforms all publicly evaluated open-source models and matches the capabilities of global closed-source giants. As the ideal foundational model for building complex agentic workflows, professional-grade software developm" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "xhigh"] + [cost] input = 1.6 output = 3.2 diff --git a/providers/novita-ai/models/deepseek/deepseek_v3.toml b/providers/novita-ai/models/deepseek/deepseek_v3.toml new file mode 100644 index 00000000000..3729307f900 --- /dev/null +++ b/providers/novita-ai/models/deepseek/deepseek_v3.toml @@ -0,0 +1,12 @@ +base_model = "deepseek/deepseek-v3" +name = "DeepSeek V3" +description = "DeepSeek-V3 is the latest model from the DeepSeek team, building upon the instruction following and coding abilities of the previous versions. Pre-trained on nearly 15 trillion tokens, the reported evaluations reveal that the model outperforms other open-source models and rivals leading closed-source models." +structured_output = false + +[cost] +input = 0.89 +output = 0.89 + +[limit] +context = 64_000 +output = 16_000 diff --git a/providers/novita-ai/models/google/gemma-3-12b-it.toml b/providers/novita-ai/models/google/gemma-3-12b-it.toml index 01e59f8e1ed..369c132e0f2 100644 --- a/providers/novita-ai/models/google/gemma-3-12b-it.toml +++ b/providers/novita-ai/models/google/gemma-3-12b-it.toml @@ -1,22 +1,14 @@ +base_model = "google/gemma-3-12b-it" name = "Gemma 3 12B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" -family = "gemma" release_date = "2025-03-13" last_updated = "2025-03-13" -attachment = true -reasoning = false -temperature = true tool_call = false -open_weights = true +structured_output = false [cost] input = 0.05 output = 0.1 [limit] -context = 131_072 output = 8_192 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/google/gemma-3-27b-it.toml b/providers/novita-ai/models/google/gemma-3-27b-it.toml index c04dda55982..15fdbe0bf3e 100644 --- a/providers/novita-ai/models/google/gemma-3-27b-it.toml +++ b/providers/novita-ai/models/google/gemma-3-27b-it.toml @@ -1,13 +1,10 @@ +base_model = "google/gemma-3-27b-it" name = "Gemma 3 27B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" -family = "gemma" release_date = "2025-03-25" last_updated = "2025-03-25" -attachment = true -reasoning = false -temperature = true tool_call = false -open_weights = true +structured_output = false [cost] input = 0.119 @@ -16,7 +13,3 @@ output = 0.2 [limit] context = 98_304 output = 16_384 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml b/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml index 1f22792e823..76e4506c1e8 100644 --- a/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml +++ b/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml @@ -1,24 +1,13 @@ +base_model = "google/gemma-4-26b-a4b-it" name = "Gemma 4 26B A4B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" -family = "gemma" -release_date = "2026-04-02" -last_updated = "2026-04-02" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.13 output = 0.4 [limit] -context = 262_144 output = 131_072 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/google/gemma-4-31b-it.toml b/providers/novita-ai/models/google/gemma-4-31b-it.toml index 6543690e3a0..859043ec420 100644 --- a/providers/novita-ai/models/google/gemma-4-31b-it.toml +++ b/providers/novita-ai/models/google/gemma-4-31b-it.toml @@ -1,24 +1,13 @@ +base_model = "google/gemma-4-31b-it" name = "Gemma 4 31B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" -family = "gemma" -release_date = "2026-04-02" -last_updated = "2026-04-02" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.14 output = 0.4 [limit] -context = 262_144 output = 131_072 - -[modalities] -input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/gryphe/mythomax-l2-13b.toml b/providers/novita-ai/models/gryphe/mythomax-l2-13b.toml index 718c181908f..b51b86333eb 100644 --- a/providers/novita-ai/models/gryphe/mythomax-l2-13b.toml +++ b/providers/novita-ai/models/gryphe/mythomax-l2-13b.toml @@ -1,4 +1,4 @@ -name = "Mythomax L2 13B" +name = "MythoMax L2 13B" description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" release_date = "2024-04-25" last_updated = "2024-04-25" @@ -6,6 +6,7 @@ attachment = false reasoning = false temperature = true tool_call = false +structured_output = false open_weights = true [cost] diff --git a/providers/novita-ai/models/inclusionai/ling-2.6-1t.toml b/providers/novita-ai/models/inclusionai/ling-2.6-1t.toml deleted file mode 100644 index f41fe3ab980..00000000000 --- a/providers/novita-ai/models/inclusionai/ling-2.6-1t.toml +++ /dev/null @@ -1,24 +0,0 @@ -name = "Ling-2.6-1T" -description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" -family = "ling" -release_date = "2026-04-23" -last_updated = "2026-06-29" -attachment = false -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.3 -output = 2.5 -cache_read = 0.06 - -[limit] -context = 262_144 -output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/inclusionai/ling-2.6-flash.toml b/providers/novita-ai/models/inclusionai/ling-2.6-flash.toml deleted file mode 100644 index ae3dd2c3c31..00000000000 --- a/providers/novita-ai/models/inclusionai/ling-2.6-flash.toml +++ /dev/null @@ -1,24 +0,0 @@ -name = "Ling-2.6-flash" -description = "Efficient model for low-latency assistance, extraction, and routine automation" -family = "ling" -release_date = "2026-04-24" -last_updated = "2026-04-24" -attachment = false -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.1 -output = 0.3 -cache_read = 0.02 - -[limit] -context = 262_144 -output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/inclusionai/ring-2.6-1t.toml b/providers/novita-ai/models/inclusionai/ring-2.6-1t.toml deleted file mode 100644 index 4555e9bb9cd..00000000000 --- a/providers/novita-ai/models/inclusionai/ring-2.6-1t.toml +++ /dev/null @@ -1,25 +0,0 @@ -name = "Ring-2.6-1T" -description = "Reasoning model for deliberate analysis, multi-step problem solving, and tool use" -family = "ring" -release_date = "2026-05-08" -last_updated = "2026-05-27" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true -structured_output = true -open_weights = false - -[cost] -input = 0.3 -output = 2.5 -cache_read = 0.06 - -[limit] -context = 262_144 -output = 65_536 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/kwaipilot/kat-coder-pro.toml b/providers/novita-ai/models/kwaipilot/kat-coder-pro.toml deleted file mode 100644 index 5cab1ba8796..00000000000 --- a/providers/novita-ai/models/kwaipilot/kat-coder-pro.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "Kat Coder Pro" -description = "Coding model for repository understanding, refactors, and agentic engineering tasks" -release_date = "2026-01-05" -last_updated = "2026-01-05" -attachment = false -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.3 -output = 1.2 -cache_read = 0.06 - -[limit] -context = 256_000 -output = 128_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/meta-llama/llama-3-70b-instruct.toml b/providers/novita-ai/models/meta-llama/llama-3-70b-instruct.toml deleted file mode 100644 index 5f90f05b22d..00000000000 --- a/providers/novita-ai/models/meta-llama/llama-3-70b-instruct.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "Llama3 70B Instruct" -description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" -release_date = "2024-04-25" -last_updated = "2024-04-25" -attachment = false -reasoning = false -temperature = true -tool_call = false -structured_output = true -open_weights = true - -[cost] -input = 0.51 -output = 0.74 - -[limit] -context = 8_192 -output = 8_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/meta-llama/llama-3-8b-instruct.toml b/providers/novita-ai/models/meta-llama/llama-3-8b-instruct.toml deleted file mode 100644 index 8c7143f0b68..00000000000 --- a/providers/novita-ai/models/meta-llama/llama-3-8b-instruct.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Llama 3 8B Instruct" -description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" -release_date = "2024-04-25" -last_updated = "2024-04-25" -attachment = false -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.04 -output = 0.04 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/meta-llama/llama-3.1-8b-instruct.toml b/providers/novita-ai/models/meta-llama/llama-3.1-8b-instruct.toml index c04dc71995b..33c5c1e6fba 100644 --- a/providers/novita-ai/models/meta-llama/llama-3.1-8b-instruct.toml +++ b/providers/novita-ai/models/meta-llama/llama-3.1-8b-instruct.toml @@ -1,13 +1,10 @@ +base_model = "meta/llama-3.1-8b-instruct" name = "Llama 3.1 8B Instruct" description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" release_date = "2024-07-24" last_updated = "2024-07-24" -attachment = false -reasoning = false -temperature = true tool_call = false -open_weights = true +structured_output = true [cost] input = 0.02 @@ -16,7 +13,3 @@ output = 0.05 [limit] context = 16_384 output = 16_384 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/meta-llama/llama-3.2-3b-instruct.toml b/providers/novita-ai/models/meta-llama/llama-3.2-3b-instruct.toml deleted file mode 100644 index ec9e35234e4..00000000000 --- a/providers/novita-ai/models/meta-llama/llama-3.2-3b-instruct.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Llama 3.2 3B Instruct" -description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" -release_date = "2024-09-18" -last_updated = "2024-09-18" -attachment = false -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.03 -output = 0.05 - -[limit] -context = 32_768 -output = 32_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/meta-llama/llama-3.3-70b-instruct.toml b/providers/novita-ai/models/meta-llama/llama-3.3-70b-instruct.toml index c1e6260cb4b..4658341f261 100644 --- a/providers/novita-ai/models/meta-llama/llama-3.3-70b-instruct.toml +++ b/providers/novita-ai/models/meta-llama/llama-3.3-70b-instruct.toml @@ -1,23 +1,14 @@ +base_model = "meta/llama-3.3-70b-instruct" name = "Llama 3.3 70B Instruct" description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" release_date = "2024-12-07" last_updated = "2024-12-07" -attachment = false -reasoning = false -temperature = true -knowledge = "2023-12" -tool_call = true -open_weights = true +structured_output = true [cost] input = 0.135 output = 0.4 [limit] -context = 131_072 -output = 120_000 - -[modalities] -input = ["text"] -output = ["text"] +context = 12_288 +output = 12_288 diff --git a/providers/novita-ai/models/meta-llama/llama-4-maverick-17b-128e-instruct-fp8.toml b/providers/novita-ai/models/meta-llama/llama-4-maverick-17b-128e-instruct-fp8.toml index c4aa16dcef9..804f0050843 100644 --- a/providers/novita-ai/models/meta-llama/llama-4-maverick-17b-128e-instruct-fp8.toml +++ b/providers/novita-ai/models/meta-llama/llama-4-maverick-17b-128e-instruct-fp8.toml @@ -6,6 +6,7 @@ attachment = true reasoning = false temperature = true tool_call = false +structured_output = true open_weights = true [cost] diff --git a/providers/novita-ai/models/meta-llama/llama-4-scout-17b-16e-instruct.toml b/providers/novita-ai/models/meta-llama/llama-4-scout-17b-16e-instruct.toml index 29aa8fa750d..9975df1cfae 100644 --- a/providers/novita-ai/models/meta-llama/llama-4-scout-17b-16e-instruct.toml +++ b/providers/novita-ai/models/meta-llama/llama-4-scout-17b-16e-instruct.toml @@ -6,6 +6,7 @@ attachment = true reasoning = false temperature = true tool_call = false +structured_output = false open_weights = true [cost] diff --git a/providers/novita-ai/models/microsoft/wizardlm-2-8x22b.toml b/providers/novita-ai/models/microsoft/wizardlm-2-8x22b.toml index 37207587026..399a16683d6 100644 --- a/providers/novita-ai/models/microsoft/wizardlm-2-8x22b.toml +++ b/providers/novita-ai/models/microsoft/wizardlm-2-8x22b.toml @@ -1,4 +1,4 @@ -name = "Wizardlm 2 8x22B" +name = "WizardLM 2 8x22B" description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" release_date = "2024-04-24" last_updated = "2024-04-24" @@ -6,6 +6,7 @@ attachment = false reasoning = false temperature = true tool_call = false +structured_output = true open_weights = true [cost] diff --git a/providers/novita-ai/models/minimax/minimax-m2.1.toml b/providers/novita-ai/models/minimax/minimax-m2.1.toml index a634f3db757..d846a31adc5 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.1.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.1.toml @@ -1,27 +1,13 @@ -name = "Minimax M2.1" +base_model = "minimax/MiniMax-M2.1" +name = "MiniMax M2.1" description = "MiniMax model for chat, coding, office work, and agentic tasks" -family = "minimax" -release_date = "2025-12-23" -last_updated = "2025-12-23" -attachment = false reasoning = false -temperature = true -tool_call = true structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" [cost] input = 0.3 output = 1.2 cache_read = 0.03 - -[limit] -context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/minimax/minimax-m2.5-highspeed.toml b/providers/novita-ai/models/minimax/minimax-m2.5-highspeed.toml index dd34c83d781..3089c344fe2 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.5-highspeed.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.5-highspeed.toml @@ -1,15 +1,15 @@ +base_model = "minimax/MiniMax-M2.5-highspeed" name = "MiniMax M2.5 Highspeed" description = "High-speed MiniMax model for low-latency coding and agent workflows" family = "minimax-m2.5" release_date = "2026-02-12" last_updated = "2026-02-12" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true structured_output = true open_weights = false +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0.6 @@ -17,12 +17,4 @@ output = 2.4 cache_read = 0.03 [limit] -context = 204_800 output = 131_100 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/minimax/minimax-m2.5.toml b/providers/novita-ai/models/minimax/minimax-m2.5.toml index 416e2c466f0..349cf1a3be6 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.5.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.5.toml @@ -1,15 +1,12 @@ +base_model = "minimax/MiniMax-M2.5" name = "MiniMax M2.5" description = "MiniMax model for chat, coding, office work, and agentic tasks" -family = "minimax" -release_date = "2026-02-12" -last_updated = "2026-02-12" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true structured_output = true open_weights = false +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0.3 @@ -17,12 +14,4 @@ output = 1.2 cache_read = 0.03 [limit] -context = 204_800 output = 131_100 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/minimax/minimax-m2.7-highspeed.toml b/providers/novita-ai/models/minimax/minimax-m2.7-highspeed.toml index 651f5b87863..e24ef6c629d 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.7-highspeed.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.7-highspeed.toml @@ -1,7 +1,9 @@ base_model = "minimax/MiniMax-M2.7-highspeed" -reasoning_options = [] +name = "MiniMax M2.7-highspeed" +description = "MiniMax M2.7-highspeed is an accelerated SOTA model engineered for scenarios demanding extreme efficiency. It perfectly inherits the core intelligence and robust digital workspace capabilities of the standard M2.7.\nIn real-world software engineering, M2.7 excels by independently driving end-to-end project delivery while efficiently handling advanced tasks such as log analysis, bug troubleshooting, code security, and machine learning. In the professional workspace, it boasts the highest open-source GDPval-AA score (1495 ELO). It delivers high-fidelity, complex editing and multi-turn revisions across the Office suite (Excel, PPT, Word), elevating task execution to industry-leading standards.\nBuilt for complex environment interactions, M2.7 maintains an impressive 97% skill-following rate even with complex, long-context tool calls (>2000 tokens). Beyond its robust productivity, M2.7 breaks the \"cold tool\" stereotype of traditional models. With exceptional identity retention and high emotional intelligence (EQ)," last_updated = "2026-05-27" structured_output = true +reasoning_options = [] [cost] input = 0.6 diff --git a/providers/novita-ai/models/minimax/minimax-m2.7.toml b/providers/novita-ai/models/minimax/minimax-m2.7.toml index daaf0eb8fd3..b6728896404 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.7.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.7.toml @@ -1,28 +1,14 @@ +base_model = "minimax/MiniMax-M2.7" name = "MiniMax M2.7" description = "MiniMax model for chat, coding, office work, and agentic tasks" family = "minimax-m2.7" -release_date = "2026-03-18" -last_updated = "2026-03-18" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true structured_output = true -open_weights = true +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0.3 output = 1.2 cache_read = 0.06 - -[limit] -context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/minimax/minimax-m2.toml b/providers/novita-ai/models/minimax/minimax-m2.toml index 108f84b5ce4..de1ee4525e5 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.toml @@ -1,27 +1,13 @@ -name = "MiniMax-M2" +base_model = "minimax/MiniMax-M2" +name = "MiniMax M2" description = "MiniMax model for chat, coding, office work, and agentic tasks" -family = "minimax" -release_date = "2025-10-27" -last_updated = "2025-10-27" -attachment = false -reasoning = true +structured_output = true reasoning_options = [] -temperature = true -tool_call = true -open_weights = true + +[interleaved] +field = "reasoning_content" [cost] input = 0.3 output = 1.2 cache_read = 0.03 - -[limit] -context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/minimaxai/minimax-m1-80k.toml b/providers/novita-ai/models/minimaxai/minimax-m1-80k.toml index 3188b27c055..4682843b775 100644 --- a/providers/novita-ai/models/minimaxai/minimax-m1-80k.toml +++ b/providers/novita-ai/models/minimaxai/minimax-m1-80k.toml @@ -5,10 +5,11 @@ release_date = "2025-06-17" last_updated = "2025-06-17" attachment = false reasoning = true -reasoning_options = [] temperature = true tool_call = true +structured_output = true open_weights = true +reasoning_options = [] [cost] input = 0.55 diff --git a/providers/novita-ai/models/mistralai/mistral-nemo.toml b/providers/novita-ai/models/mistralai/mistral-nemo.toml index 558a70af9a7..3faebd2b17a 100644 --- a/providers/novita-ai/models/mistralai/mistral-nemo.toml +++ b/providers/novita-ai/models/mistralai/mistral-nemo.toml @@ -1,14 +1,9 @@ -name = "Mistral Nemo" +base_model = "mistral/mistral-nemo" description = "Mistral model for multilingual chat, reasoning, and tool-assisted workflows" -family = "mistral-nemo" release_date = "2024-07-30" last_updated = "2024-07-30" -attachment = false -reasoning = false -temperature = true tool_call = false structured_output = true -open_weights = true [cost] input = 0.04 @@ -17,7 +12,3 @@ output = 0.17 [limit] context = 60_288 output = 16_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/moonshotai/kimi-k2-0905.toml b/providers/novita-ai/models/moonshotai/kimi-k2-0905.toml index f2b1db3e75a..95ca6a54035 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2-0905.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2-0905.toml @@ -1,4 +1,4 @@ -name = "Kimi K2 0905" +name = "Kimi K2 Instruct 0905" description = "Kimi model for long-context chat, coding, and agentic reasoning" family = "kimi-k2" release_date = "2025-09-05" @@ -6,9 +6,9 @@ last_updated = "2025-09-05" attachment = false reasoning = false temperature = true -knowledge = "2024-10" tool_call = true structured_output = true +knowledge = "2024-10" open_weights = true [cost] @@ -17,7 +17,7 @@ output = 2.5 [limit] context = 262_144 -output = 262_144 +output = 98_304 [modalities] input = ["text"] diff --git a/providers/novita-ai/models/moonshotai/kimi-k2-instruct.toml b/providers/novita-ai/models/moonshotai/kimi-k2-instruct.toml index 12b184ebf8e..150208c9996 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2-instruct.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2-instruct.toml @@ -6,6 +6,7 @@ attachment = false reasoning = false temperature = true tool_call = true +structured_output = false open_weights = true [cost] @@ -14,7 +15,7 @@ output = 2.3 [limit] context = 131_072 -output = 32_768 +output = 98_304 [modalities] input = ["text"] diff --git a/providers/novita-ai/models/moonshotai/kimi-k2-thinking.toml b/providers/novita-ai/models/moonshotai/kimi-k2-thinking.toml index e7f187e8a3d..cb9ed265d4c 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2-thinking.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2-thinking.toml @@ -1,15 +1,12 @@ -name = "Kimi K2 Thinking" +base_model = "moonshotai/kimi-k2-thinking" description = "Kimi reasoning model for long-horizon research, planning, and tool use" -family = "kimi-thinking" release_date = "2025-11-07" last_updated = "2026-06-29" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true structured_output = true -open_weights = true +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0.6 @@ -17,12 +14,4 @@ output = 2.5 cache_read = 0.15 [limit] -context = 262_144 -output = 262_144 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] +output = 98_304 diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.5.toml b/providers/novita-ai/models/moonshotai/kimi-k2.5.toml index 2596e658e6f..42e46485b00 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.5.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.5.toml @@ -1,29 +1,16 @@ -name = "Kimi K2.5" +base_model = "moonshotai/kimi-k2.5" description = "Kimi multimodal agent model for visual understanding, coding, and planning" -family = "kimi-k2" release_date = "2026-01-27" last_updated = "2026-01-27" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] temperature = true -tool_call = true -structured_output = true -knowledge = "2025-01" -open_weights = true - -[cost] -input = 0.6 -output = 3.0 -cache_read = 0.1 - -[limit] -context = 262_144 -output = 262_144 [interleaved] field = "reasoning_content" -[modalities] -input = ["text", "image", "video"] -output = ["text"] +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.6 +output = 3 +cache_read = 0.1 diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.6.toml b/providers/novita-ai/models/moonshotai/kimi-k2.6.toml index 37df0417686..43dfa3a1c27 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.6.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.6.toml @@ -1,9 +1,12 @@ base_model = "moonshotai/kimi-k2.6" -reasoning_options = [{ type = "toggle" }] +description = "Kimi K2.6 is an open-source, native multimodal agentic model that significantly advances practical capabilities in long-horizon coding, coding-driven design, and swarm-based task orchestration. It robustly executes complex, end-to-end development tasks across multiple programming languages and domains, seamlessly transforming simple prompts and visual inputs into production-ready, aesthetically precise interfaces and full-stack workflows. Uniquely engineered for high scalability, K2.6 can horizontally orchestrate up to 300 domain-specialized sub-agents through 4,000 coordinated steps, dynamically decomposing intricate tasks to deliver diverse end-to-end outputs—from documents and spreadsheets to fully functional websites—in a single autonomous run. Furthermore, its proactive execution capabilities empower persistent, 24/7 background agents to manage schedules, deploy code, and orchestrate cross-platform operations entirely without human oversight, establishing it as a premier foundational model for next-gener" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [cost] input = 0.8 output = 3.4 diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml b/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml index e420a46934a..eeafdb61965 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml @@ -1,16 +1,10 @@ base_model = "moonshotai/kimi-k2.7-code" +description = "Kimi K2.7 Code is MoonshotAI's strongest coding & agentic model — a 1T-parameter MoE (32B activated) , 256K context and interleaved thinking with multi-step tool calling. It delivers major gains on long-horizon coding tasks while cutting thinking-token usage by ~30% vs K2.6, and accepts text, image and video inputs for vision-driven development workflows." [[reasoning_options]] type = "toggle" [cost] input = 0.95 -output = 4.00 +output = 4 cache_read = 0.19 - -[limit] -context = 262_144 -output = 262_144 - -[modalities] -input = ["text", "image", "video"] diff --git a/providers/novita-ai/models/moonshotai/kimi-k3.toml b/providers/novita-ai/models/moonshotai/kimi-k3.toml index f398ea072a2..47646f26271 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k3.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k3.toml @@ -1,4 +1,5 @@ base_model = "moonshotai/kimi-k3" +description = "Kimi K3 is Kimi’s most capable model to date, with 2.8 trillion parameters. Built on Kimi Delta Attention, a hybrid linear attention mechanism, and Attention Residuals, it offers native visual understanding and a 1M-token context window for frontier intelligence scenarios such as software engineering, knowledge work, and deep reasoning." [[reasoning_options]] type = "toggle" @@ -8,13 +9,9 @@ type = "effort" values = ["low", "high", "max"] [cost] -input = 3.00 -output = 15.00 +input = 3 +output = 15 cache_read = 0.3 [limit] -context = 1_048_576 output = 1_048_576 - -[modalities] -input = ["text", "image", "video"] diff --git a/providers/novita-ai/models/openai/gpt-oss-120b.toml b/providers/novita-ai/models/openai/gpt-oss-120b.toml index f13f4b73585..5843c35c7aa 100644 --- a/providers/novita-ai/models/openai/gpt-oss-120b.toml +++ b/providers/novita-ai/models/openai/gpt-oss-120b.toml @@ -1,23 +1,17 @@ +base_model = "openai/gpt-oss-120b" name = "OpenAI GPT OSS 120B" description = "Open-weight GPT model for self-hosted reasoning and instruction-following workloads" release_date = "2025-08-06" last_updated = "2025-08-06" attachment = true -reasoning = true -reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 0.05 output = 0.25 -[limit] -context = 131_072 -output = 32_768 - [modalities] input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/openai/gpt-oss-20b.toml b/providers/novita-ai/models/openai/gpt-oss-20b.toml index 0fdae660ba1..1a78017e2f3 100644 --- a/providers/novita-ai/models/openai/gpt-oss-20b.toml +++ b/providers/novita-ai/models/openai/gpt-oss-20b.toml @@ -1,23 +1,18 @@ -name = "OpenAI: GPT OSS 20B" +base_model = "openai/gpt-oss-20b" +name = "OpenAI GPT OSS 20B" description = "Open-weight GPT model for self-hosted reasoning and instruction-following workloads" release_date = "2025-08-06" last_updated = "2025-08-06" attachment = true -reasoning = true -reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }] -temperature = true tool_call = false -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 0.04 output = 0.15 -[limit] -context = 131_072 -output = 32_768 - [modalities] input = ["text", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/paddlepaddle/paddleocr-vl.toml b/providers/novita-ai/models/paddlepaddle/paddleocr-vl.toml index 6dfce843e12..ca561217db9 100644 --- a/providers/novita-ai/models/paddlepaddle/paddleocr-vl.toml +++ b/providers/novita-ai/models/paddlepaddle/paddleocr-vl.toml @@ -1,4 +1,4 @@ -name = "PaddleOCR-VL" +name = "PaddleOCR VL" description = "Multimodal model for analyzing text, images, documents, and rich media" release_date = "2025-10-22" last_updated = "2025-10-22" @@ -6,6 +6,7 @@ attachment = true reasoning = false temperature = true tool_call = false +structured_output = false open_weights = true [cost] diff --git a/providers/novita-ai/models/qwen/qwen-2.5-72b-instruct.toml b/providers/novita-ai/models/qwen/qwen-2.5-72b-instruct.toml index 6dfd1b9d503..fe7fc44be04 100644 --- a/providers/novita-ai/models/qwen/qwen-2.5-72b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen-2.5-72b-instruct.toml @@ -1,4 +1,4 @@ -name = "Qwen 2.5 72B Instruct" +name = "Qwen2.5 72B Instruct" description = "Qwen instruction model for multilingual chat, reasoning, and tool use" family = "qwen" release_date = "2024-10-15" @@ -6,9 +6,9 @@ last_updated = "2024-10-15" attachment = false reasoning = false temperature = true -knowledge = "2024-04" tool_call = true structured_output = true +knowledge = "2024-04" open_weights = true [cost] diff --git a/providers/novita-ai/models/qwen/qwen-mt-plus.toml b/providers/novita-ai/models/qwen/qwen-mt-plus.toml index 8591838a922..59f1cd1a28d 100644 --- a/providers/novita-ai/models/qwen/qwen-mt-plus.toml +++ b/providers/novita-ai/models/qwen/qwen-mt-plus.toml @@ -6,6 +6,7 @@ attachment = false reasoning = false temperature = true tool_call = false +structured_output = false open_weights = true [cost] diff --git a/providers/novita-ai/models/qwen/qwen2.5-7b-instruct.toml b/providers/novita-ai/models/qwen/qwen2.5-7b-instruct.toml deleted file mode 100644 index a7e408e56f0..00000000000 --- a/providers/novita-ai/models/qwen/qwen2.5-7b-instruct.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen2.5 7B Instruct" -description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -release_date = "2025-04-16" -last_updated = "2025-04-16" -attachment = false -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.07 -output = 0.07 - -[limit] -context = 32_000 -output = 32_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen2.5-vl-72b-instruct.toml b/providers/novita-ai/models/qwen/qwen2.5-vl-72b-instruct.toml deleted file mode 100644 index c48fdbc92cb..00000000000 --- a/providers/novita-ai/models/qwen/qwen2.5-vl-72b-instruct.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen2.5 VL 72B Instruct" -description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -family = "qwen" -release_date = "2025-03-25" -last_updated = "2025-03-25" -attachment = true -reasoning = false -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.8 -output = 0.8 - -[limit] -context = 32_768 -output = 32_768 - -[modalities] -input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-235b-a22b-fp8.toml b/providers/novita-ai/models/qwen/qwen3-235b-a22b-fp8.toml index 5794dec5f57..adaeb474860 100644 --- a/providers/novita-ai/models/qwen/qwen3-235b-a22b-fp8.toml +++ b/providers/novita-ai/models/qwen/qwen3-235b-a22b-fp8.toml @@ -4,10 +4,11 @@ release_date = "2025-04-29" last_updated = "2025-04-29" attachment = false reasoning = true -reasoning_options = [] temperature = true tool_call = false +structured_output = true open_weights = true +reasoning_options = [] [cost] input = 0.2 diff --git a/providers/novita-ai/models/qwen/qwen3-235b-a22b-instruct-2507.toml b/providers/novita-ai/models/qwen/qwen3-235b-a22b-instruct-2507.toml index a260dd0396f..4e21bba938e 100644 --- a/providers/novita-ai/models/qwen/qwen3-235b-a22b-instruct-2507.toml +++ b/providers/novita-ai/models/qwen/qwen3-235b-a22b-instruct-2507.toml @@ -1,15 +1,9 @@ +base_model = "alibaba/qwen3-235b-a22b-instruct-2507" name = "Qwen3 235B A22B Instruct 2507" description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -family = "qwen" release_date = "2025-07-22" last_updated = "2025-07-22" -attachment = false -reasoning = false -temperature = true -knowledge = "2025-04" -tool_call = true structured_output = true -open_weights = true [cost] input = 0.09 @@ -17,8 +11,3 @@ output = 0.58 [limit] context = 131_072 -output = 16_384 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-235b-a22b-thinking-2507.toml b/providers/novita-ai/models/qwen/qwen3-235b-a22b-thinking-2507.toml index bf9b1f12b4a..531cf1a29da 100644 --- a/providers/novita-ai/models/qwen/qwen3-235b-a22b-thinking-2507.toml +++ b/providers/novita-ai/models/qwen/qwen3-235b-a22b-thinking-2507.toml @@ -1,19 +1,20 @@ -name = "Qwen3 235B A22b Thinking 2507" +name = "Qwen3 235B A22B Thinking 2507" description = "Qwen reasoning model for deliberate problem solving, math, and coding" family = "qwen" release_date = "2025-07-25" last_updated = "2025-07-25" attachment = false reasoning = true -reasoning_options = [] temperature = true -knowledge = "2025-04" tool_call = true +structured_output = false +knowledge = "2025-04" open_weights = true +reasoning_options = [] [cost] input = 0.3 -output = 3.0 +output = 3 [limit] context = 131_072 diff --git a/providers/novita-ai/models/qwen/qwen3-30b-a3b-fp8.toml b/providers/novita-ai/models/qwen/qwen3-30b-a3b-fp8.toml deleted file mode 100644 index 04d55d6b9a8..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-30b-a3b-fp8.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen3 30B A3B" -description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -release_date = "2025-04-29" -last_updated = "2025-04-29" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.09 -output = 0.45 - -[limit] -context = 40_960 -output = 20_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-32b-fp8.toml b/providers/novita-ai/models/qwen/qwen3-32b-fp8.toml deleted file mode 100644 index cf31ae85636..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-32b-fp8.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen3 32B" -description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -release_date = "2025-04-29" -last_updated = "2025-04-29" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.1 -output = 0.45 - -[limit] -context = 40_960 -output = 20_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-4b-fp8.toml b/providers/novita-ai/models/qwen/qwen3-4b-fp8.toml deleted file mode 100644 index 939cd8cee7f..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-4b-fp8.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen3 4B" -description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -release_date = "2025-04-29" -last_updated = "2025-04-29" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.03 -output = 0.03 - -[limit] -context = 128_000 -output = 20_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-8b-fp8.toml b/providers/novita-ai/models/qwen/qwen3-8b-fp8.toml deleted file mode 100644 index 4b5c5d01dec..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-8b-fp8.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Qwen3 8B" -description = "Qwen instruction model for multilingual chat, reasoning, and tool use" -release_date = "2025-04-29" -last_updated = "2025-04-29" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = false -open_weights = true - -[cost] -input = 0.035 -output = 0.138 - -[limit] -context = 128_000 -output = 20_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-coder-30b-a3b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-coder-30b-a3b-instruct.toml index 69602c25a80..0085921423d 100644 --- a/providers/novita-ai/models/qwen/qwen3-coder-30b-a3b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-coder-30b-a3b-instruct.toml @@ -1,13 +1,9 @@ -name = "Qwen3 Coder 30b A3B Instruct" +base_model = "alibaba/qwen3-coder-30b-a3b-instruct" +name = "Qwen3 Coder 30B A3B Instruct" description = "Qwen coding model for software agents, repository edits, and code reasoning" release_date = "2025-10-09" last_updated = "2025-10-09" -attachment = false -reasoning = false -temperature = true -tool_call = true structured_output = true -open_weights = true [cost] input = 0.07 @@ -16,7 +12,3 @@ output = 0.27 [limit] context = 160_000 output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-coder-480b-a35b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-coder-480b-a35b-instruct.toml index c45aca69cae..89784201ef0 100644 --- a/providers/novita-ai/models/qwen/qwen3-coder-480b-a35b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-coder-480b-a35b-instruct.toml @@ -1,24 +1,10 @@ +base_model = "alibaba/qwen3-coder-480b-a35b-instruct" name = "Qwen3 Coder 480B A35B Instruct" description = "Qwen coding model for software agents, repository edits, and code reasoning" -family = "qwen" release_date = "2025-07-23" last_updated = "2025-07-23" -attachment = false -reasoning = false -temperature = true -knowledge = "2025-04" -tool_call = true structured_output = true -open_weights = true [cost] input = 0.38 output = 1.55 - -[limit] -context = 262_144 -output = 65_536 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-coder-next.toml b/providers/novita-ai/models/qwen/qwen3-coder-next.toml index 9ad4ec23a79..5a9d65800be 100644 --- a/providers/novita-ai/models/qwen/qwen3-coder-next.toml +++ b/providers/novita-ai/models/qwen/qwen3-coder-next.toml @@ -1,23 +1,6 @@ -name = "Qwen3 Coder Next" +base_model = "alibaba/qwen3-coder-next" description = "Qwen coding model for software agents, repository edits, and code reasoning" -family = "qwen" -release_date = "2026-02-03" -last_updated = "2026-02-03" -attachment = false -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true [cost] input = 0.2 output = 1.5 - -[limit] -context = 262_144 -output = 65_536 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-max.toml b/providers/novita-ai/models/qwen/qwen3-max.toml index c25a772eb91..7aec29d3120 100644 --- a/providers/novita-ai/models/qwen/qwen3-max.toml +++ b/providers/novita-ai/models/qwen/qwen3-max.toml @@ -1,24 +1,21 @@ -name = "Qwen3 Max" +base_model = "alibaba/qwen3-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows" -family = "qwen" release_date = "2025-09-24" last_updated = "2025-09-24" -attachment = false -reasoning = false -temperature = true -knowledge = "2025-04" -tool_call = true +reasoning = true structured_output = true -open_weights = false +reasoning_options = [] [cost] -input = 2.11 -output = 8.45 +input = 0.845 +output = 3.38 -[limit] -context = 262_144 -output = 65_536 +[[cost.tiers]] +tier = { type = "context", size = 32_768 } +input = 1.4 +output = 5.64 -[modalities] -input = ["text"] -output = ["text"] +[[cost.tiers]] +tier = { type = "context", size = 131_072 } +input = 2.11 +output = 8.45 diff --git a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml index 554db812d4c..566507f5689 100644 --- a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml @@ -1,22 +1,12 @@ +base_model = "alibaba/qwen3-next-80b-a3b-instruct" name = "Qwen3 Next 80B A3B Instruct" description = "Qwen instruction model for multilingual chat, reasoning, and tool use" release_date = "2025-09-10" last_updated = "2025-09-10" -attachment = false -reasoning = false -temperature = true -tool_call = true +reasoning = true structured_output = true -open_weights = true +reasoning_options = [] [cost] input = 0.15 output = 1.5 - -[limit] -context = 131_072 -output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-thinking.toml b/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-thinking.toml deleted file mode 100644 index 873ac60137b..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-thinking.toml +++ /dev/null @@ -1,23 +0,0 @@ -name = "Qwen3 Next 80B A3B Thinking" -description = "Qwen reasoning model for deliberate problem solving, math, and coding" -release_date = "2025-09-10" -last_updated = "2025-09-10" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.15 -output = 1.5 - -[limit] -context = 131_072 -output = 32_768 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-instruct.toml index 5a7c4c6f0f9..70779edc665 100644 --- a/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-instruct.toml @@ -1,22 +1,11 @@ -name = "Qwen3 VL 235B A22B Instruct" +base_model = "alibaba/qwen3-vl-235b-a22b-instruct" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" release_date = "2025-09-24" last_updated = "2025-09-24" -attachment = true -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true [cost] input = 0.3 output = 1.5 -[limit] -context = 131_072 -output = 32_768 - [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-thinking.toml b/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-thinking.toml index d3b458466c6..32e610767f9 100644 --- a/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-thinking.toml +++ b/providers/novita-ai/models/qwen/qwen3-vl-235b-a22b-thinking.toml @@ -1,22 +1,13 @@ -name = "Qwen3 VL 235B A22B Thinking" +base_model = "alibaba/qwen3-vl-235b-a22b-thinking" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" release_date = "2025-09-24" last_updated = "2025-09-24" -attachment = true -reasoning = true +structured_output = false reasoning_options = [] -temperature = true -tool_call = false -open_weights = true [cost] input = 0.98 output = 3.95 -[limit] -context = 131_072 -output = 32_768 - [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-instruct.toml index 92b9d517e9d..f5341a30d09 100644 --- a/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-instruct.toml @@ -1,4 +1,4 @@ -name = "qwen/qwen3-vl-30b-a3b-instruct" +name = "Qwen3 VL 30B A3B Instruct" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" release_date = "2025-10-11" last_updated = "2025-10-11" diff --git a/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-thinking.toml b/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-thinking.toml deleted file mode 100644 index 1d82f3168cf..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-vl-30b-a3b-thinking.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "qwen/qwen3-vl-30b-a3b-thinking" -description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -release_date = "2025-10-11" -last_updated = "2025-10-11" -attachment = true -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.2 -output = 1.0 - -[limit] -context = 131_072 -output = 32_768 - -[modalities] -input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3-vl-8b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-vl-8b-instruct.toml deleted file mode 100644 index b2855bb05e3..00000000000 --- a/providers/novita-ai/models/qwen/qwen3-vl-8b-instruct.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "qwen/qwen3-vl-8b-instruct" -description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -release_date = "2025-10-17" -last_updated = "2025-10-17" -attachment = true -reasoning = false -temperature = true -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.08 -output = 0.5 - -[limit] -context = 131_072 -output = 32_768 - -[modalities] -input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml b/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml index e8529355c39..671ce23baaf 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml @@ -1,24 +1,15 @@ -name = "Qwen3.5-122B-A10B" +base_model = "alibaba/qwen3.5-122b-a10b" +name = "Qwen3.5 122B A10B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -family = "qwen" release_date = "2026-02-26" last_updated = "2026-02-26" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.4 output = 3.2 -[limit] -context = 262_144 -output = 65_536 - [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3.5-27b.toml b/providers/novita-ai/models/qwen/qwen3.5-27b.toml index 370ab051428..b463ab43a8b 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-27b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-27b.toml @@ -1,24 +1,14 @@ -name = "Qwen3.5-27B" +base_model = "alibaba/qwen3.5-27b" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -family = "qwen" release_date = "2026-02-26" last_updated = "2026-02-26" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.3 output = 2.4 -[limit] -context = 262_144 -output = 65_536 - [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml b/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml index a691cc1925c..f9517d02f8e 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml @@ -1,24 +1,15 @@ -name = "Qwen3.5-35B-A3B" +base_model = "alibaba/qwen3.5-35b-a3b" +name = "Qwen3.5 35B A3B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -family = "qwen" release_date = "2026-02-26" last_updated = "2026-02-26" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.25 -output = 2.0 - -[limit] -context = 262_144 -output = 65_536 +output = 2 [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml b/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml index 43800444813..5afda7de2a4 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml @@ -1,24 +1,15 @@ -name = "Qwen3.5-397B-A17B" +base_model = "alibaba/qwen3.5-397b-a17b" +name = "Qwen3.5 397B A17B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" -family = "qwen" release_date = "2026-02-17" last_updated = "2026-02-17" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.6 output = 3.6 -[limit] -context = 262_144 -output = 64_000 - [modalities] input = ["text", "image", "video"] -output = ["text"] diff --git a/providers/novita-ai/models/qwen/qwen3.7-max.toml b/providers/novita-ai/models/qwen/qwen3.7-max.toml index 363a8ba3b78..19857d4c77c 100644 --- a/providers/novita-ai/models/qwen/qwen3.7-max.toml +++ b/providers/novita-ai/models/qwen/qwen3.7-max.toml @@ -1,26 +1,13 @@ -name = "Qwen3.7-Max" +base_model = "alibaba/qwen3.7-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows" -family = "qwen" -release_date = "2026-05-21" last_updated = "2026-05-27" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -open_weights = false + +[[reasoning_options]] +type = "toggle" [cost] input = 1.25 output = 3.75 cache_read = 0.25 cache_write = 1.5625 - -[limit] -context = 1_000_000 -output = 65_536 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/sao10K/L3-8B-stheno-v3.2.toml b/providers/novita-ai/models/sao10K/L3-8B-stheno-v3.2.toml deleted file mode 100644 index 04efbfb5da6..00000000000 --- a/providers/novita-ai/models/sao10K/L3-8B-stheno-v3.2.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "L3 8B Stheno V3.2" -description = "Open Llama instruction model for multilingual chat, reasoning, and coding" -family = "llama" -release_date = "2024-11-29" -last_updated = "2024-11-29" -attachment = false -reasoning = false -temperature = true -tool_call = true -open_weights = true - -[cost] -input = 0.05 -output = 0.05 - -[limit] -context = 8_192 -output = 32_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/sao10K/l3-70b-euryale-v2.1.toml b/providers/novita-ai/models/sao10K/l3-70b-euryale-v2.1.toml deleted file mode 100644 index 900d618f62c..00000000000 --- a/providers/novita-ai/models/sao10K/l3-70b-euryale-v2.1.toml +++ /dev/null @@ -1,21 +0,0 @@ -name = "L3 70B Euryale V2.1 " -description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" -release_date = "2024-06-18" -last_updated = "2024-06-18" -attachment = false -reasoning = false -temperature = true -tool_call = true -open_weights = true - -[cost] -input = 1.48 -output = 1.48 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/sao10K/l3-8b-lunaris.toml b/providers/novita-ai/models/sao10K/l3-8b-lunaris.toml deleted file mode 100644 index 57b6a8bf655..00000000000 --- a/providers/novita-ai/models/sao10K/l3-8b-lunaris.toml +++ /dev/null @@ -1,22 +0,0 @@ -name = "Sao10k L3 8B Lunaris " -description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" -release_date = "2024-11-28" -last_updated = "2024-11-28" -attachment = false -reasoning = false -temperature = true -tool_call = false -structured_output = true -open_weights = true - -[cost] -input = 0.05 -output = 0.05 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/sao10K/l31-70b-euryale-v2.2.toml b/providers/novita-ai/models/sao10K/l31-70b-euryale-v2.2.toml deleted file mode 100644 index a28c8d7cf82..00000000000 --- a/providers/novita-ai/models/sao10K/l31-70b-euryale-v2.2.toml +++ /dev/null @@ -1,21 +0,0 @@ -name = "L31 70B Euryale V2.2" -description = "Open-weight instruction model for adaptable chat and self-hosted production workloads" -release_date = "2024-09-19" -last_updated = "2024-09-19" -attachment = false -reasoning = false -temperature = true -tool_call = true -open_weights = true - -[cost] -input = 1.48 -output = 1.48 - -[limit] -context = 8_192 -output = 8_192 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/xiaomimimo/mimo-v2-flash.toml b/providers/novita-ai/models/xiaomimimo/mimo-v2-flash.toml deleted file mode 100644 index e7d77465208..00000000000 --- a/providers/novita-ai/models/xiaomimimo/mimo-v2-flash.toml +++ /dev/null @@ -1,26 +0,0 @@ -name = "XiaomiMiMo/MiMo-V2-Flash" -description = "MiMo flash model for fast multimodal assistance and agent workflows" -family = "mimo" -release_date = "2025-12-19" -last_updated = "2025-12-19" -attachment = false -reasoning = true -reasoning_options = [] -temperature = true -knowledge = "2024-12" -tool_call = true -structured_output = true -open_weights = true - -[cost] -input = 0.1 -output = 0.3 -cache_read = 0.30 - -[limit] -context = 262_144 -output = 32_000 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/xiaomimimo/mimo-v2-pro.toml b/providers/novita-ai/models/xiaomimimo/mimo-v2-pro.toml deleted file mode 100644 index 54890ef6778..00000000000 --- a/providers/novita-ai/models/xiaomimimo/mimo-v2-pro.toml +++ /dev/null @@ -1,18 +0,0 @@ -base_model = "xiaomi/mimo-v2-pro" -reasoning_options = [] -last_updated = "2026-05-27" -structured_output = true - -[interleaved] -field = "reasoning_content" - -[cost] -input = 2 -output = 6 -cache_read = 0.4 - -[[cost.tiers]] -tier = { type = "context", size = 256_000 } -input = 2 -output = 6 -cache_read = 0.4 diff --git a/providers/novita-ai/models/xiaomimimo/mimo-v2.5-pro.toml b/providers/novita-ai/models/xiaomimimo/mimo-v2.5-pro.toml index 5a921aebd83..60075519318 100644 --- a/providers/novita-ai/models/xiaomimimo/mimo-v2.5-pro.toml +++ b/providers/novita-ai/models/xiaomimimo/mimo-v2.5-pro.toml @@ -1,7 +1,9 @@ base_model = "xiaomi/mimo-v2.5-pro" -reasoning_options = [] +name = "Xiaomi MiMo V2.5 Pro" +description = "MiMo-V2.5-Pro is purpose-built to push the boundaries of complex software engineering and extreme long-horizon tasks. Compared to its predecessor, it achieves a comprehensive leap in general agentic capabilities, advancing the human-AI collaboration paradigm toward true \"autonomous delivery.\" Without human intervention, it stably orchestrates massive workflows requiring up to a thousand tool calls in a single session, not only precisely capturing implicit requirements within ultra-long contexts but also demonstrating exceptional global architectural planning and self-correction discipline. In core agentic scenarios and long-horizon complexities, MiMo-V2.5-Pro is fully equipped to go head-to-head with top-tier global models like Claude Opus 4.6 and GPT-5.4. Backed by this exceptionally high execution confidence and long-term logical consistency, it completely sheds the \"co-pilot\" label, ready to take on truly serious, professional-grade workloads in real-world business environments." last_updated = "2026-05-27" structured_output = true +reasoning_options = [] [interleaved] field = "reasoning_content" @@ -10,9 +12,3 @@ field = "reasoning_content" input = 0.522 output = 1.044 cache_read = 0.0043 - -[[cost.tiers]] -tier = { type = "context", size = 256_000 } -input = 0.522 -output = 1.044 -cache_read = 0.0043 diff --git a/providers/novita-ai/models/zai-org/autoglm-phone-9b-multilingual.toml b/providers/novita-ai/models/zai-org/autoglm-phone-9b-multilingual.toml index 8a8347e2a94..47864645abe 100644 --- a/providers/novita-ai/models/zai-org/autoglm-phone-9b-multilingual.toml +++ b/providers/novita-ai/models/zai-org/autoglm-phone-9b-multilingual.toml @@ -1,4 +1,4 @@ -name = "AutoGLM-Phone-9B-Multilingual" +name = "AutoGLM Phone 9B Multilingual" description = "GLM vision model for visual reasoning, documents, and multimodal agents" release_date = "2025-12-10" last_updated = "2025-12-10" @@ -6,6 +6,7 @@ attachment = true reasoning = false temperature = true tool_call = false +structured_output = false open_weights = true [cost] diff --git a/providers/novita-ai/models/zai-org/glm-4.5-air.toml b/providers/novita-ai/models/zai-org/glm-4.5-air.toml index ec8f2f1d61b..3e3363de82b 100644 --- a/providers/novita-ai/models/zai-org/glm-4.5-air.toml +++ b/providers/novita-ai/models/zai-org/glm-4.5-air.toml @@ -1,25 +1,14 @@ +base_model = "zhipuai/glm-4.5-air" name = "GLM 4.5 Air" description = "Efficient GLM model for fast reasoning, coding, and agent workflows" -family = "glm-air" release_date = "2025-10-13" last_updated = "2025-10-13" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -knowledge = "2025-04" -tool_call = true -open_weights = true +structured_output = false + +[[reasoning_options]] +type = "toggle" [cost] input = 0.13 output = 0.85 cache_read = 0.025 - -[limit] -context = 131_072 -output = 98_304 - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.5.toml b/providers/novita-ai/models/zai-org/glm-4.5.toml deleted file mode 100644 index 55ddf8ee121..00000000000 --- a/providers/novita-ai/models/zai-org/glm-4.5.toml +++ /dev/null @@ -1,29 +0,0 @@ -name = "GLM-4.5" -description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" -family = "glm" -release_date = "2025-07-28" -last_updated = "2025-07-28" -attachment = false -reasoning = true -temperature = true -tool_call = true -open_weights = true - -[[reasoning_options]] -type = "toggle" - -[cost] -input = 0.6 -output = 2.2 -cache_read = 0.11 - -[limit] -context = 131_072 -output = 98_304 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.5v.toml b/providers/novita-ai/models/zai-org/glm-4.5v.toml index 6a106f1cbd9..929071c1119 100644 --- a/providers/novita-ai/models/zai-org/glm-4.5v.toml +++ b/providers/novita-ai/models/zai-org/glm-4.5v.toml @@ -1,25 +1,16 @@ +base_model = "zhipuai/glm-4.5v" name = "GLM 4.5V" description = "GLM vision model for visual reasoning, documents, and multimodal agents" family = "glmv" -release_date = "2025-08-11" -last_updated = "2025-08-11" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -knowledge = "2025-04" -tool_call = true structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.6 output = 1.8 cache_read = 0.11 + [limit] context = 65_536 -output = 16_384 - -[modalities] -input = ["text", "video", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.6.toml b/providers/novita-ai/models/zai-org/glm-4.6.toml index 0e09c434022..38cbbf95b21 100644 --- a/providers/novita-ai/models/zai-org/glm-4.6.toml +++ b/providers/novita-ai/models/zai-org/glm-4.6.toml @@ -1,28 +1,15 @@ +base_model = "zhipuai/glm-4.6" name = "GLM 4.6" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" -family = "glm" -release_date = "2025-09-30" -last_updated = "2025-09-30" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.55 output = 2.2 cache_read = 0.11 - -[limit] -context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.6v.toml b/providers/novita-ai/models/zai-org/glm-4.6v.toml index f906825058d..010c72f7d6c 100644 --- a/providers/novita-ai/models/zai-org/glm-4.6v.toml +++ b/providers/novita-ai/models/zai-org/glm-4.6v.toml @@ -1,16 +1,11 @@ +base_model = "zhipuai/glm-4.6v" name = "GLM 4.6V" description = "GLM vision model for visual reasoning, documents, and multimodal agents" family = "glmv" -release_date = "2025-12-08" -last_updated = "2025-12-08" -attachment = true -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -knowledge = "2025-04" -tool_call = true structured_output = true -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.3 @@ -19,8 +14,3 @@ cache_read = 0.055 [limit] context = 131_072 -output = 32_768 - -[modalities] -input = ["text", "video", "image"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml index b5e02bf1ccb..cc252317148 100644 --- a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml +++ b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml @@ -1,16 +1,11 @@ -name = "GLM-4.7-Flash" +base_model = "zhipuai/glm-4.7-flash" +name = "GLM 4.7 Flash" description = "Efficient GLM model for fast reasoning, coding, and agent workflows" family = "glm" -release_date = "2026-01-19" -last_updated = "2026-01-19" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -knowledge = "2025-04" -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.07 @@ -18,10 +13,4 @@ output = 0.4 cache_read = 0.01 [limit] -context = 200_000 output = 128_000 - - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-4.7.toml b/providers/novita-ai/models/zai-org/glm-4.7.toml index b8fc51214fd..9d14eb4f926 100644 --- a/providers/novita-ai/models/zai-org/glm-4.7.toml +++ b/providers/novita-ai/models/zai-org/glm-4.7.toml @@ -1,28 +1,15 @@ -name = "GLM-4.7" +base_model = "zhipuai/glm-4.7" +name = "GLM 4.7" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" -family = "glm" -release_date = "2025-12-22" -last_updated = "2025-12-22" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.6 output = 2.2 cache_read = 0.11 - -[limit] -context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-5.1.toml b/providers/novita-ai/models/zai-org/glm-5.1.toml index 50d4249122e..72b58f0d69e 100644 --- a/providers/novita-ai/models/zai-org/glm-5.1.toml +++ b/providers/novita-ai/models/zai-org/glm-5.1.toml @@ -1,15 +1,14 @@ -name = "GLM-5.1" +base_model = "zhipuai/glm-5.1" +name = "GLM 5.1" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" -family = "glm" release_date = "2026-03-27" last_updated = "2026-03-27" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true -structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 1.38 @@ -18,11 +17,3 @@ cache_read = 0.26 [limit] context = 204_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/providers/novita-ai/models/zai-org/glm-5.2.toml b/providers/novita-ai/models/zai-org/glm-5.2.toml index daa973cd32b..48867f07756 100644 --- a/providers/novita-ai/models/zai-org/glm-5.2.toml +++ b/providers/novita-ai/models/zai-org/glm-5.2.toml @@ -1,5 +1,9 @@ -name = "GLM-5.2" base_model = "zhipuai/glm-5.2" +name = "GLM 5.2" +description = "GLM-5.2 is Z.AI's latest flagship model, meticulously engineered for long-horizon autonomous tasks. Capable of working continuously on a single assignment for up to 8 hours, it autonomously manages the entire workflow—from initial planning and execution to iterative optimization and the delivery of production-grade results. With coding and agentic capabilities that rival leading proprietary frontier models, it excels particularly in sustained execution, complex engineering optimization, and real-world development scenarios. Its context window has been expanded from 200K to 1M tokens, making it an ideal foundational model for powering advanced autonomous agents and long-horizon coding assistants." + +[interleaved] +field = "reasoning_content" [[reasoning_options]] type = "effort" @@ -12,7 +16,3 @@ cache_read = 0.26 [limit] context = 1_048_576 -output = 131_072 - -[interleaved] -field = "reasoning_content" diff --git a/providers/novita-ai/models/zai-org/glm-5.toml b/providers/novita-ai/models/zai-org/glm-5.toml index 7b9123aa63a..e0eb6b2b390 100644 --- a/providers/novita-ai/models/zai-org/glm-5.toml +++ b/providers/novita-ai/models/zai-org/glm-5.toml @@ -1,28 +1,19 @@ -name = "GLM-5" +base_model = "zhipuai/glm-5" +name = "GLM 5" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" -family = "glm" release_date = "2026-02-11" -last_updated = "2026-02-12" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -open_weights = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] -input = 1.0 +input = 1 output = 3.2 cache_read = 0.2 [limit] context = 202_800 -output = 131_072 - -[interleaved] -field = "reasoning_content" - -[modalities] -input = ["text"] -output = ["text"] diff --git a/sync.md b/sync.md index 06ceb1b2a42..3db5ef6a722 100644 --- a/sync.md +++ b/sync.md @@ -163,6 +163,13 @@ CrossModel is implemented in `packages/core/src/sync/providers/crossmodel.ts`. - `structured_output` comes from `capabilities.json`; when that field is absent, the sync preserves an existing authored override. - Other intrinsic model facts remain inherited from the canonical `base_model` metadata. +## Novita AI Notes + +- Novita AI uses the authenticated `https://api.novita.ai/openai/v1/models` endpoint as the complete served-model catalog. Set `NOVITA_API_KEY` locally or in the hourly workflow. +- The endpoint supplies current pricing, modalities, features, and limits; authored audio/reasoning prices and cache prices missing from its response remain intact. Tier-specific optional prices are retained only for an identical context threshold. Explicit zero input/output prices with no pricing or tiers mean a free model. +- Existing authored descriptions and provider-specific reasoning controls remain curated: the API can truncate descriptions and does not describe each model's reasoning wire controls. New models lacking verified lab metadata, usable prices, or reasoning controls are skipped and tracked through deduped missing-model issues in GitHub Actions. +- An empty response is rejected; a run removing more than half the existing catalog fails before any files are written. Smaller removals follow the complete remote catalog. + ## OpenRouter Notes OpenRouter is implemented in `packages/core/src/sync/providers/openrouter.ts`. From 3dc8eeb6f4df529620d45d6edaeca10ea277c62d Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 16:55:39 +0800 Subject: [PATCH 09/17] feat(novita-ai): sync DeepSeek V4.1 Flash with verified reasoning toggle --- packages/core/src/sync/providers/novita-ai.ts | 2 ++ packages/core/test/novita-ai.test.ts | 30 +++++++++++++++++++ .../models/deepseek/deepseek-v4.1-flash.toml | 20 +++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 64854cda5b5..e2f14f90f27 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -145,6 +145,8 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi }; if (baseModel !== undefined) return factorBaseModel(baseModel, { ...values, + // An empty catalog description is not a provider-specific override. + description: existing?.description || model.description || undefined, // These are lab facts, not claims made by the Novita catalog endpoint. open_weights: existing?.open_weights, release_date: existing?.release_date, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 829166f4f95..28939b77a8b 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -196,6 +196,36 @@ test("Novita AI sync reuses a verified lab alias and fixed R1 controls", () => { }), context)?.model).toMatchObject({ base_model: "deepseek/deepseek-r1", reasoning_options: [] }); }); +test("Novita AI sync updates V4.1 Flash prices while retaining its verified toggle", () => { + const authored: ExistingModel = { + base_model: "deepseek/deepseek-v4.1-flash", + reasoning_options: [{ type: "toggle" }], + interleaved: { field: "reasoning_content" }, + cost: { input: 1, output: 2 }, + }; + const translated = novitaAi.translateModel(novitaAiModel({ + id: "deepseek/deepseek-v4.1-flash", + context_size: 1_048_576, + max_output_tokens: 393_216, + features: ["reasoning", "function-calling", "structured-outputs"], + input_modalities: ["text", "image"], + output_modalities: ["text"], + pricing: { + prompt: { price_per_m_decimal: "0.3" }, + completion: { price_per_m_decimal: "1.2" }, + input_cache_read: { price_per_m_decimal: "0.006" }, + }, + }), { authored: () => authored, existing: () => authored }); + expect(translated?.model).toMatchObject({ + base_model: authored.base_model, + reasoning_options: [{ type: "toggle" }], + interleaved: { field: "reasoning_content" }, + cost: { input: 0.3, output: 1.2, cache_read: 0.006 }, + limit: { context: 1_048_576, output: 393_216 }, + }); + expect(translated?.model).not.toHaveProperty("description"); +}); + test("Novita AI sync maps tiered context prices and cache-write", () => { const pricing = (input: string, output: string, cacheWrite: string) => ({ prompt: { price_per_m_decimal: input }, diff --git a/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml new file mode 100644 index 00000000000..b0518c72f69 --- /dev/null +++ b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml @@ -0,0 +1,20 @@ +# Toggle: thinking.type = enabled|disabled +# Verified on 2026-09-17 with Novita chat/completions: enabled returns +# reasoning_content/reasoning_tokens; disabled returns neither. +# Pricing and limits: https://novita.ai/models/model-detail/deepseek-deepseek-v4.1-flash +base_model = "deepseek/deepseek-v4.1-flash" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.3 +output = 1.2 +cache_read = 0.006 + +[limit] +context = 1_048_576 +output = 393_216 From d9941f7bbd289c734245833bf9dbc1dfa19e83e3 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 17 Sep 2026 17:14:17 +0800 Subject: [PATCH 10/17] feat(novita-ai): sync models with verified thinking controls --- packages/core/src/sync/providers/novita-ai.ts | 42 ++++++++++++++++--- packages/core/test/novita-ai.test.ts | 17 +++++++- .../deepseek/deepseek-v4-flash-0731.toml | 19 +++++++++ .../deepseek-v4-flash-vision-exp.toml | 19 +++++++++ .../inclusionai/ling-3.0-flash-fin.toml | 15 +++++++ .../novita-ai/models/minimax/minimax-m3.toml | 26 ++++++++++++ .../nvidia/nemotron-3-nano-30b-a3b.toml | 19 +++++++++ .../novita-ai/models/qwen/qwen3.5-plus.toml | 22 ++++++++++ .../novita-ai/models/qwen/qwen3.6-27b.toml | 17 ++++++++ .../models/qwen/qwen3.6-35b-a3b.toml | 18 ++++++++ .../novita-ai/models/qwen/qwen3.6-plus.toml | 25 +++++++++++ .../novita-ai/models/qwen/qwen3.8-27b.toml | 18 ++++++++ .../novita-ai/models/qwen/qwen3.8-flash.toml | 15 +++++++ .../novita-ai/models/qwen/qwen3.8-max.toml | 19 +++++++++ providers/novita-ai/models/tencent/hy3.toml | 21 ++++++++++ .../novita-ai/models/zai-org/glm-5-turbo.toml | 18 ++++++++ .../novita-ai/models/zai-org/glm-5.3.toml | 18 ++++++++ .../models/zai-org/glm-5v-turbo.toml | 22 ++++++++++ sync.md | 1 + 19 files changed, 364 insertions(+), 7 deletions(-) create mode 100644 providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml create mode 100644 providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml create mode 100644 providers/novita-ai/models/inclusionai/ling-3.0-flash-fin.toml create mode 100644 providers/novita-ai/models/minimax/minimax-m3.toml create mode 100644 providers/novita-ai/models/nvidia/nemotron-3-nano-30b-a3b.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.5-plus.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.6-27b.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.6-plus.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.8-27b.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.8-flash.toml create mode 100644 providers/novita-ai/models/qwen/qwen3.8-max.toml create mode 100644 providers/novita-ai/models/tencent/hy3.toml create mode 100644 providers/novita-ai/models/zai-org/glm-5-turbo.toml create mode 100644 providers/novita-ai/models/zai-org/glm-5.3.toml create mode 100644 providers/novita-ai/models/zai-org/glm-5v-turbo.toml diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index e2f14f90f27..5ec74003cfd 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -8,6 +8,27 @@ const API_ENDPOINT = "https://api.novita.ai/openai/v1/models"; const BASE_MODEL_ALIASES: Record = { "deepseek/deepseek_v3": "deepseek/deepseek-v3", }; +// Verified per model with Novita chat/completions: disabling thinking removes +// reasoning_content, while enabling it returns reasoning_content. +const VERIFIED_THINKING_TOGGLE = new Set([ + "deepseek/deepseek-v4-flash-0731", + "deepseek/deepseek-v4-flash-vision-exp", + "inclusionai/ling-3.0-flash-fin", + "minimax/minimax-m3", + "nvidia/nemotron-3-nano-30b-a3b", + "qwen/qwen3.5-plus", + "qwen/qwen3.6-27b", + "qwen/qwen3.6-35b-a3b", + "qwen/qwen3.6-plus", + "qwen/qwen3.8-27b", + "qwen/qwen3.8-flash", + "qwen/qwen3.8-max", + "tencent/hy3", + "zai-org/glm-5-turbo", + "zai-org/glm-5.3", + "zai-org/glm-5v-turbo", +]); +const VERIFIED_TOGGLE_HEADER = "# Toggle: thinking.type = enabled|disabled\n# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content.\n"; const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); const Pricing = z.object({ prompt: Price.optional(), @@ -124,12 +145,17 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const context = model.context_size ?? resolved?.limit?.context ?? 0; const outputLimit = model.max_output_tokens ?? resolved?.limit?.output ?? context; const modelCost = cost(model, existing); + // Novita's GLM-5.3 description claims reasoning cannot be disabled, but + // its chat API returns no reasoning when thinking.type is disabled. + const description = model.id === "zai-org/glm-5.3" ? undefined : model.description; // DeepSeek R1 is fixed-reasoning on Novita, as with its already curated R1 variants. - const reasoningOptions = existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); + const reasoningOptions = existing?.reasoning_options + ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] : model.id === "deepseek/deepseek-r1" ? [] : undefined); + const interleaved = existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, - description: existing?.description || model.description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), + description: existing?.description || description || describeModel({ id: model.id, name, reasoning, tool_call: toolCall, structured_output: structuredOutput || undefined, open_weights: existing?.open_weights ?? false, limit: { context, output: outputLimit }, modalities: { input, output } }), family: existing?.family, release_date: existing?.release_date ?? dateFromTimestamp(model.created), last_updated: existing?.last_updated ?? dateFromTimestamp(model.created), @@ -146,20 +172,20 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi if (baseModel !== undefined) return factorBaseModel(baseModel, { ...values, // An empty catalog description is not a provider-specific override. - description: existing?.description || model.description || undefined, + description: existing?.description || description || undefined, // These are lab facts, not claims made by the Novita catalog endpoint. open_weights: existing?.open_weights, release_date: existing?.release_date, last_updated: existing?.last_updated, temperature: existing?.temperature, reasoning_options: reasoningOptions, - interleaved: existing?.interleaved, + interleaved, }, values.limit, existing?.base_model_omit); return { ...existing, ...values, reasoning_options: existing?.reasoning_options, - interleaved: existing?.interleaved, + interleaved, status: existing?.status, knowledge: existing?.knowledge, } as SyncedModel; @@ -205,6 +231,10 @@ export const novitaAi = { }, translateModel(model, context) { const translated = buildNovitaModel(model, context.authored(model.id), context.existing(model.id)); - return translated === undefined ? undefined : { id: model.id, model: translated }; + return translated === undefined ? undefined : { + id: model.id, + model: translated, + header: VERIFIED_THINKING_TOGGLE.has(model.id) ? VERIFIED_TOGGLE_HEADER : undefined, + }; }, } satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 28939b77a8b..42859af8066 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -161,7 +161,8 @@ test("Novita AI sync treats explicit zero prices without tiers as free", () => { output_token_price_per_m: 0, features: ["reasoning"], }); - expect(novitaAi.translateModel(model, { existing: () => undefined, authored: () => undefined })).toBeUndefined(); + expect(novitaAi.translateModel(model, { existing: () => undefined, authored: () => undefined })?.model) + .toMatchObject({ base_model: "inclusionai/ling-3.0-flash-fin", reasoning_options: [{ type: "toggle" }], cost: { input: 0, output: 0 } }); const authored = { base_model: "inclusionai/ling-3.0-flash-fin", reasoning_options: [] }; const translated = novitaAi.translateModel(model, { existing: () => authored, authored: () => authored }); expect(translated?.model).toMatchObject({ @@ -226,6 +227,20 @@ test("Novita AI sync updates V4.1 Flash prices while retaining its verified togg expect(translated?.model).not.toHaveProperty("description"); }); +test("Novita AI sync creates only explicitly verified new reasoners", () => { + const context = { authored: () => undefined, existing: () => undefined }; + const pricing = { prompt: { price_per_m_decimal: "0.15" }, completion: { price_per_m_decimal: "0.5" } }; + for (const id of ["qwen/qwen3.8-flash", "minimax/minimax-m3", "zai-org/glm-5.3", "deepseek/deepseek-v4-flash-0731"]) { + const translated = novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context); + expect(translated?.model).toMatchObject({ reasoning_options: [{ type: "toggle" }], interleaved: { field: "reasoning_content" } }); + expect(translated?.header).toContain("thinking.type = enabled|disabled"); + if (id === "zai-org/glm-5.3") expect(translated?.model).not.toHaveProperty("description"); + } + for (const id of ["zai-org/glm-5.3-flash", "deepseek/deepseek-v4-pro-0813", "qwen/qwen3.8-2.4t-a95b"]) { + expect(novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context)).toBeUndefined(); + } +}); + test("Novita AI sync maps tiered context prices and cache-write", () => { const pricing = (input: string, output: string, cacheWrite: string) => ({ prompt: { price_per_m_decimal: input }, diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml new file mode 100644 index 00000000000..3e83d5bf90d --- /dev/null +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml @@ -0,0 +1,19 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "deepseek/deepseek-v4-flash-0731" +description = "DeepSeek V4 Flash 0731 is a sparse mixture-of-experts model from DeepSeek, with 13B active parameters out of 284B total. This re-post-trained revision is suited for coding, reasoning, and agent workflows." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.44 +output = 1.32 +cache_read = 0.028 + +[limit] +context = 1_048_576 +output = 393_216 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml new file mode 100644 index 00000000000..32c2b130742 --- /dev/null +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml @@ -0,0 +1,19 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "deepseek/deepseek-v4-flash-vision-exp" +description = "DeepSeek V4 Flash Vision Exp is an experimental vision-enabled version of DeepSeek V4 Flash 0731(opens in new tab) from DeepSeek, adding image understanding while matching the base model on text capabilities including agents, reasoning, and world knowledge. It is a sparse mixture-of-experts model with 13B active parameters out of 284B total.\n\nIt is suited for document and chart understanding, visual question answering, and multimodal agent workflows that interleave text and images." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.44 +output = 1.32 +cache_read = 0.028 + +[limit] +context = 1_048_576 +output = 393_216 diff --git a/providers/novita-ai/models/inclusionai/ling-3.0-flash-fin.toml b/providers/novita-ai/models/inclusionai/ling-3.0-flash-fin.toml new file mode 100644 index 00000000000..8bb72ae435f --- /dev/null +++ b/providers/novita-ai/models/inclusionai/ling-3.0-flash-fin.toml @@ -0,0 +1,15 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "inclusionai/ling-3.0-flash-fin" +description = "Ling-3.0-flash-Fin is a finance-enhanced MoE model built on Ling-3.0-flash, with 124 billion total parameters and approximately 5.1 billion activated parameters. Designed for real-world investment workflows, it is optimized for complex multi-step tasks and long-horizon planning and execution. With a relatively small active parameter footprint, it delivers competitive financial performance while maintaining strong general capabilities in reasoning, coding, and mathematics." +structured_output = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0 +output = 0 diff --git a/providers/novita-ai/models/minimax/minimax-m3.toml b/providers/novita-ai/models/minimax/minimax-m3.toml new file mode 100644 index 00000000000..7362ab73da3 --- /dev/null +++ b/providers/novita-ai/models/minimax/minimax-m3.toml @@ -0,0 +1,26 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "minimax/MiniMax-M3" +name = "MiniMax M3" +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.3 +output = 1.2 +cache_read = 0.06 + +[[cost.tiers]] +tier = { type = "context", size = 524_288 } +input = 0.6 +output = 2.4 +cache_read = 0.12 + +[limit] +context = 1_000_000 +output = 131_072 diff --git a/providers/novita-ai/models/nvidia/nemotron-3-nano-30b-a3b.toml b/providers/novita-ai/models/nvidia/nemotron-3-nano-30b-a3b.toml new file mode 100644 index 00000000000..3ba22fd26f0 --- /dev/null +++ b/providers/novita-ai/models/nvidia/nemotron-3-nano-30b-a3b.toml @@ -0,0 +1,19 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "nvidia/nemotron-3-nano-30b-a3b" +name = "NVIDIA Nemotron 3 Nano 30B A3B" +description = "Nemotron-3-Nano-30B-A3B is NVIDIA's compute-efficient, open-weight reasoning model built for agentic AI. Its Mixture-of-Experts design (30B total / 3.5B active) with a hybrid Mamba-2 + Transformer architecture delivers strong reasoning and tool use at a fraction of the compute required by comparable dense models. It supports a 256K context window, toggleable chain-of-thought reasoning, function calling, and structured (JSON) outputs — making it well-suited for long-context agents, coding, and math. Released under the NVIDIA Nemotron Open Model License and ready for commercial use." +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.05 +output = 0.2 + +[limit] +output = 32_768 diff --git a/providers/novita-ai/models/qwen/qwen3.5-plus.toml b/providers/novita-ai/models/qwen/qwen3.5-plus.toml new file mode 100644 index 00000000000..37b217ebee1 --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.5-plus.toml @@ -0,0 +1,22 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.5-plus" +name = "Qwen3.5-Plus" +description = "The Qwen3.5 native vision-language series Plus models are based on a hybrid architecture design that integrates linear attention mechanisms with sparse Mixture-of-Experts (MoE), achieving higher inference efficiency. Across various task evaluations, the 3.5 series demonstrates exceptional performance comparable to current top-tier frontier models, marking a leap forward in both plain text and multimodal capabilities compared to the 3 series." +attachment = true +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.4 +output = 2.4 + +[[cost.tiers]] +tier = { type = "context", size = 256_000 } +input = 0.5 +output = 3 diff --git a/providers/novita-ai/models/qwen/qwen3.6-27b.toml b/providers/novita-ai/models/qwen/qwen3.6-27b.toml new file mode 100644 index 00000000000..1aaf646daa9 --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.6-27b.toml @@ -0,0 +1,17 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.6-27b" +description = "The Qwen3.6 27B native vision-language dense model builds upon the 3.5-27B version, with key improvements in agentic coding capabilities and enhanced STEM reasoning and inference skills. In the vision modality, it demonstrates significant advances in spatial intelligence, object localization, and detection, while video understanding, document OCR, and visual agent capabilities continue to improve steadily." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.6 +output = 3.6 + +[modalities] +input = ["text", "image", "video"] diff --git a/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml b/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml new file mode 100644 index 00000000000..3109c405547 --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml @@ -0,0 +1,18 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.6-35b-a3b" +name = "Qwen3.6 35B A3B" +description = "The Qwen3.6 35B-A3B native vision-language model is built on a hybrid architecture that integrates linear attention mechanisms with a sparse mixture-of-experts framework, achieving higher inference efficiency. Compared with the 3.5-35B-A3B, this model demonstrates significantly improved agentic coding capabilities, mathematical and code reasoning abilities, spatial intelligence, as well as object localization and object detection performance." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.248 +output = 1.485 + +[modalities] +input = ["text", "image", "video"] diff --git a/providers/novita-ai/models/qwen/qwen3.6-plus.toml b/providers/novita-ai/models/qwen/qwen3.6-plus.toml new file mode 100644 index 00000000000..091c9df58b0 --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.6-plus.toml @@ -0,0 +1,25 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.6-plus" +name = "Qwen3.6-Plus" +description = "The Qwen3.6 native vision-language Plus series models demonstrate exceptional performance on par with the current state-of-the-art models, with a significant improvement in overall results compared to the 3.5 series. The models have been markedly enhanced in code-related capabilities such as agentic coding, front-end programming, and Vibe coding, as well as in multi-modal general object recognition, OCR, and object localization." +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.5 +output = 3 +cache_read = 0.05 +cache_write = 0.625 + +[[cost.tiers]] +tier = { type = "context", size = 262_144 } +input = 2 +output = 6 +cache_read = 0.2 +cache_write = 2.5 diff --git a/providers/novita-ai/models/qwen/qwen3.8-27b.toml b/providers/novita-ai/models/qwen/qwen3.8-27b.toml new file mode 100644 index 00000000000..386fa956bae --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.8-27b.toml @@ -0,0 +1,18 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.8-27b" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.42 +output = 3 +cache_read = 0.085 + +[limit] +context = 1_000_000 +output = 131_072 diff --git a/providers/novita-ai/models/qwen/qwen3.8-flash.toml b/providers/novita-ai/models/qwen/qwen3.8-flash.toml new file mode 100644 index 00000000000..d62bff12c91 --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.8-flash.toml @@ -0,0 +1,15 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.8-flash" +description = "Qwen3.8-Flash is Alibaba's multimodal MoE model and an early preview of the Qwen4 architecture: 125B total parameters with only 6B activated per token, plus a 51B N-gram embedding, built on GDN + QSA hybrid attention. It accepts text, image and video input across a 1M-token context and emits up to 131K tokens, with thinking mode on by default and switchable off. Built for coding, agentic workflows, visual\nand long-document understanding, and long-video analysis at a fraction of the cost of comparable frontier models." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.15 +output = 0.47 +cache_read = 0.016 diff --git a/providers/novita-ai/models/qwen/qwen3.8-max.toml b/providers/novita-ai/models/qwen/qwen3.8-max.toml new file mode 100644 index 00000000000..15ada490ebb --- /dev/null +++ b/providers/novita-ai/models/qwen/qwen3.8-max.toml @@ -0,0 +1,19 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "alibaba/qwen3.8-max" +description = " A 2.4T-parameter MoE flagship for coding and knowledge work. Programs autonomously for days to deliver complete projects end to end, with native visual understanding across planning, execution, and verification, plus deep semantic analysis of ultra-long documents and video." +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 2 +output = 6 +cache_read = 0.25 + +[modalities] +input = ["text", "image", "video"] diff --git a/providers/novita-ai/models/tencent/hy3.toml b/providers/novita-ai/models/tencent/hy3.toml new file mode 100644 index 00000000000..8db740c9212 --- /dev/null +++ b/providers/novita-ai/models/tencent/hy3.toml @@ -0,0 +1,21 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "tencent/hy3" +base_model_omit = ["limit.input"] +description = "Built for real-world business scenarios, Hy3 features a 295B/21B active MoE architecture, native 256K context support, and three reasoning modes. It enhances coding, long-form comprehension, multi-turn dialogue, and agentic task execution, balancing reliability, efficiency, and cost across both high-frequency interactions and complex workflows." +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.14 +output = 0.58 +cache_read = 0.035 + +[limit] +context = 262_144 +output = 262_144 diff --git a/providers/novita-ai/models/zai-org/glm-5-turbo.toml b/providers/novita-ai/models/zai-org/glm-5-turbo.toml new file mode 100644 index 00000000000..0a5d408f945 --- /dev/null +++ b/providers/novita-ai/models/zai-org/glm-5-turbo.toml @@ -0,0 +1,18 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "zhipuai/glm-5-turbo" +description = "GLM-5 is an open-source foundation model engineered for complex system engineering and long-horizon Agent tasks, delivering reliable productivity for top-tier programmers. Transcending the boundary from \"writing code\" to \"building systems,\" it moves beyond traditional snippet generation to offer senior-architect-level planning and execution capabilities. By rejecting the \"frontend-heavy, logic-light\" approach, GLM-5 demonstrates exceptional reasoning and self-healing abilities in backend refactoring, complex algorithm implementation, and deep debugging—autonomously analyzing logs and iteratively fixing persistent bugs until the system runs. As the first open-source model featuring Opus-class style and system engineering depth, GLM-5 provides extreme logic density alongside the freedom of local deployment and high cost-effectiveness, making it the ideal choice for large-scale backend development and automated Agent construction." + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 1.2 +output = 4 +cache_read = 0.24 + +[limit] +context = 202_800 diff --git a/providers/novita-ai/models/zai-org/glm-5.3.toml b/providers/novita-ai/models/zai-org/glm-5.3.toml new file mode 100644 index 00000000000..5593ada64cd --- /dev/null +++ b/providers/novita-ai/models/zai-org/glm-5.3.toml @@ -0,0 +1,18 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "zhipuai/glm-5.3" +name = "GLM 5.3" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 1.4 +output = 4.4 +cache_read = 0.26 + +[limit] +context = 1_048_576 diff --git a/providers/novita-ai/models/zai-org/glm-5v-turbo.toml b/providers/novita-ai/models/zai-org/glm-5v-turbo.toml new file mode 100644 index 00000000000..4a66fb28df5 --- /dev/null +++ b/providers/novita-ai/models/zai-org/glm-5v-turbo.toml @@ -0,0 +1,22 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. +base_model = "zhipuai/glm-5v-turbo" +description = "GLM-5V-Turbo is Z.AI’s first multimodal coding foundation model, built for vision-based coding tasks. It can natively process multimodal inputs such as images, video, and text, while also excelling at long-horizon planning, complex coding, and action execution. Deeply optimized for agent workflows, it works seamlessly with agents such as Claude Code and OpenClaw to complete the full loop of “understand the environment → plan actions → execute tasks”." +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 1.2 +output = 4 +cache_read = 0.24 + +[limit] +context = 204_800 + +[modalities] +input = ["text", "image", "video"] diff --git a/sync.md b/sync.md index 3db5ef6a722..22a4ce2fe04 100644 --- a/sync.md +++ b/sync.md @@ -168,6 +168,7 @@ CrossModel is implemented in `packages/core/src/sync/providers/crossmodel.ts`. - Novita AI uses the authenticated `https://api.novita.ai/openai/v1/models` endpoint as the complete served-model catalog. Set `NOVITA_API_KEY` locally or in the hourly workflow. - The endpoint supplies current pricing, modalities, features, and limits; authored audio/reasoning prices and cache prices missing from its response remain intact. Tier-specific optional prices are retained only for an identical context threshold. Explicit zero input/output prices with no pricing or tiers mean a free model. - Existing authored descriptions and provider-specific reasoning controls remain curated: the API can truncate descriptions and does not describe each model's reasoning wire controls. New models lacking verified lab metadata, usable prices, or reasoning controls are skipped and tracked through deduped missing-model issues in GitHub Actions. +- New reasoning models use an exact-ID list of live-tested `thinking.type = enabled|disabled` controls. The same field is ignored by some Novita routes (including GLM-5.3 Flash and DeepSeek V4 Pro 0813), so it must not be inferred for an entire lab or from a `reasoning` feature flag. Unknown effort levels are not published. - An empty response is rejected; a run removing more than half the existing catalog fails before any files are written. Smaller removals follow the complete remote catalog. ## OpenRouter Notes From 0ab93c0bdc7af1e56260322a390212f31dbc1382 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 13:35:18 +0800 Subject: [PATCH 11/17] fix(novita-ai): tolerate zero context catalog entries --- packages/core/src/sync/providers/novita-ai.ts | 7 ++++-- packages/core/test/novita-ai.test.ts | 4 ++++ .../models/zai-org/glm-4.7-flash.toml | 22 +++++-------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 5ec74003cfd..265cff76f90 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -47,7 +47,8 @@ export const NovitaAIModel = z.object({ title: z.string().optional(), display_name: z.string().optional(), description: z.string().optional(), - context_size: z.number().int().positive().optional(), + // Some non-LLM catalog entries use zero when no context window applies. + context_size: z.number().int().nonnegative().optional(), max_output_tokens: z.number().int().positive().optional(), features: z.array(z.string()).optional(), input_modalities: z.array(z.string()).optional(), @@ -142,7 +143,9 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const reasoning = features?.has("reasoning") ?? resolved?.reasoning ?? false; const toolCall = features?.has("function-calling") ?? resolved?.tool_call ?? false; const structuredOutput = features?.has("structured-outputs") ?? resolved?.structured_output ?? false; - const context = model.context_size ?? resolved?.limit?.context ?? 0; + const context = model.context_size && model.context_size > 0 + ? model.context_size + : resolved?.limit?.context ?? 0; const outputLimit = model.max_output_tokens ?? resolved?.limit?.output ?? context; const modelCost = cost(model, existing); // Novita's GLM-5.3 description claims reasoning cannot be disabled, but diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 42859af8066..b18dffd9c80 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -32,6 +32,10 @@ test("accepts the standard OpenAI list marker when present", () => { expect(NovitaAIResponse.parse({ object: "list", data: [novitaAiModel()] }).data).toHaveLength(1); }); +test("accepts Novita non-LLM catalog entries with zero context size", () => { + expect(NovitaAIResponse.parse({ data: [novitaAiModel({ id: "image/design", context_size: 0 })] }).data[0]?.context_size).toBe(0); +}); + test("maps Novita catalog metadata onto existing models", () => { const translated = novitaAi.translateModel(novitaAiModel({ display_name: "GLM 5.3 Flash", diff --git a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml index c0079c3decf..b6b76b98fe1 100644 --- a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml +++ b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml @@ -1,16 +1,10 @@ -name = "GLM-4.7-Flash" +base_model = "zhipuai/glm-4.7-flash" +name = "GLM 4.7 Flash" description = "Efficient GLM model for fast reasoning, coding, and agent workflows" -family = "glm-flash" -release_date = "2026-01-19" -last_updated = "2026-01-19" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -temperature = true -tool_call = true structured_output = true -knowledge = "2025-04" -open_weights = true + +[[reasoning_options]] +type = "toggle" [cost] input = 0.07 @@ -18,10 +12,4 @@ output = 0.4 cache_read = 0.01 [limit] -context = 200_000 output = 128_000 - - -[modalities] -input = ["text"] -output = ["text"] From 318642d26f6a0f5e9c0043d8a300bfdba1dc9231 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 14:03:22 +0800 Subject: [PATCH 12/17] fix(novita-ai): address automated review findings --- packages/core/src/sync/providers/novita-ai.ts | 30 ++++++++++++++----- packages/core/test/novita-ai.test.ts | 29 ++++++++++++++++-- .../novita-ai/models/qwen/qwen3-max.toml | 9 +++++- .../qwen/qwen3-next-80b-a3b-instruct.toml | 2 -- sync.md | 1 + 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 265cff76f90..966ad04d73e 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -23,11 +23,15 @@ const VERIFIED_THINKING_TOGGLE = new Set([ "qwen/qwen3.8-27b", "qwen/qwen3.8-flash", "qwen/qwen3.8-max", + "qwen/qwen3-max", "tencent/hy3", "zai-org/glm-5-turbo", "zai-org/glm-5.3", "zai-org/glm-5v-turbo", ]); +const VERIFIED_NON_REASONING = new Set([ + "qwen/qwen3-next-80b-a3b-instruct", +]); const VERIFIED_TOGGLE_HEADER = "# Toggle: thinking.type = enabled|disabled\n# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content.\n"; const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); const Pricing = z.object({ @@ -51,6 +55,8 @@ export const NovitaAIModel = z.object({ context_size: z.number().int().nonnegative().optional(), max_output_tokens: z.number().int().positive().optional(), features: z.array(z.string()).optional(), + model_type: z.string().optional(), + endpoints: z.array(z.string()).optional(), input_modalities: z.array(z.string()).optional(), output_modalities: z.array(z.string()).optional(), pricing: Pricing.optional(), @@ -140,9 +146,13 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const input = modalities(model.input_modalities, resolved?.modalities?.input) ?? ["text"]; const output = modalities(model.output_modalities, resolved?.modalities?.output) ?? ["text"]; const features = model.features === undefined ? undefined : new Set(model.features); - const reasoning = features?.has("reasoning") ?? resolved?.reasoning ?? false; - const toolCall = features?.has("function-calling") ?? resolved?.tool_call ?? false; - const structuredOutput = features?.has("structured-outputs") ?? resolved?.structured_output ?? false; + const featureValue = (feature: string, fallback: boolean | undefined) => + features === undefined || features.size === 0 + ? fallback ?? false + : features.has(feature) || fallback === true; + const reasoning = VERIFIED_NON_REASONING.has(model.id) ? false : featureValue("reasoning", resolved?.reasoning); + const toolCall = featureValue("function-calling", resolved?.tool_call); + const structuredOutput = featureValue("structured-outputs", resolved?.structured_output); const context = model.context_size && model.context_size > 0 ? model.context_size : resolved?.limit?.context ?? 0; @@ -152,9 +162,10 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi // its chat API returns no reasoning when thinking.type is disabled. const description = model.id === "zai-org/glm-5.3" ? undefined : model.description; // DeepSeek R1 is fixed-reasoning on Novita, as with its already curated R1 variants. - const reasoningOptions = existing?.reasoning_options - ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] : model.id === "deepseek/deepseek-r1" ? [] : undefined); - const interleaved = existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); + const reasoningOptions = VERIFIED_NON_REASONING.has(model.id) ? undefined + : VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] + : existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); + const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, @@ -216,6 +227,9 @@ export const novitaAi = { trackMissingModels: true, maxMissingFraction: 0.5, missingModelID(model) { + // The endpoint also exposes image, embedding, and other non-chat rows. + // Only chat models are candidates for a provider catalog TOML/issue. + if (model.model_type !== "chat" || !model.endpoints?.includes("chat/completions") || model.context_size === 0) return undefined; return model.id; }, sourceID(model) { @@ -237,7 +251,9 @@ export const novitaAi = { return translated === undefined ? undefined : { id: model.id, model: translated, - header: VERIFIED_THINKING_TOGGLE.has(model.id) ? VERIFIED_TOGGLE_HEADER : undefined, + header: ("reasoning_options" in translated && translated.reasoning_options?.some((option) => option.type === "toggle")) + ? VERIFIED_TOGGLE_HEADER + : undefined, }; }, } satisfies SyncProvider; diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index b18dffd9c80..1a6be9a9489 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -245,6 +245,30 @@ test("Novita AI sync creates only explicitly verified new reasoners", () => { } }); +test("Novita AI sync inherits capabilities from partial feature lists", () => { + const authored: ExistingModel = { + name: "DeepSeek", description: "DeepSeek", attachment: false, open_weights: true, + limit: { context: 1000, output: 100 }, modalities: { input: ["text"], output: ["text"] }, + base_model: undefined, reasoning: true, tool_call: true, + reasoning_options: [{ type: "toggle" }], + }; + const translated = novitaAi.translateModel(novitaAiModel({ id: "novita/custom", features: ["serverless"] }), { + authored: () => authored, existing: () => authored, + }); + expect(translated?.model).toMatchObject({ reasoning: true, tool_call: true, reasoning_options: [{ type: "toggle" }] }); +}); + +test("Novita AI sync treats verified Qwen reasoning behavior per model", () => { + const price = { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } }; + const context = { authored: () => undefined, existing: () => undefined }; + expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-max", features: ["reasoning"], pricing: price }), context)?.model) + .toMatchObject({ reasoning: true, reasoning_options: [{ type: "toggle" }] }); + expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-next-80b-a3b-instruct", features: ["reasoning"], pricing: price }), context)?.model) + .not.toHaveProperty("reasoning_options"); + expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-next-80b-a3b-instruct", features: ["reasoning"], pricing: price }), context)?.model) + .not.toHaveProperty("reasoning_options"); +}); + test("Novita AI sync maps tiered context prices and cache-write", () => { const pricing = (input: string, output: string, cacheWrite: string) => ({ prompt: { price_per_m_decimal: input }, @@ -382,7 +406,7 @@ test("Novita AI sync keeps local files when translation skips an existing remote await Bun.write(file, content); const result = await syncProvider({ ...novitaAi, modelsDir, - async fetchModels() { return { data: [novitaAiModel({ id: "novita/custom" })] }; }, + async fetchModels() { return { data: [novitaAiModel({ id: "novita/custom", model_type: "chat", endpoints: ["chat/completions"] })] }; }, translateModel() { return undefined; }, }, { dryRun: true, openIssues: true }); expect(result.deleted).toBe(0); @@ -400,7 +424,8 @@ test("Novita AI sync tracks remote-only IDs", () => { expect(novitaAi.sourceID?.(novitaAiModel())).toBe("deepseek/deepseek-v3.2"); expect(novitaAi.sourceID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); expect(novitaAi.trackMissingModels).toBe(true); - expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); + expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/new-model", model_type: "chat", endpoints: ["chat/completions"] }))).toBe("novita/new-model"); + expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/image", model_type: "image", endpoints: ["images/generations"] }))).toBeUndefined(); }); test("Novita AI sync requires NOVITA_API_KEY", async () => { diff --git a/providers/novita-ai/models/qwen/qwen3-max.toml b/providers/novita-ai/models/qwen/qwen3-max.toml index 7aec29d3120..f1eedc42d82 100644 --- a/providers/novita-ai/models/qwen/qwen3-max.toml +++ b/providers/novita-ai/models/qwen/qwen3-max.toml @@ -1,10 +1,17 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows" release_date = "2025-09-24" last_updated = "2025-09-24" reasoning = true structured_output = true -reasoning_options = [] + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.845 diff --git a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml b/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml index 566507f5689..58f9f62bb00 100644 --- a/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml +++ b/providers/novita-ai/models/qwen/qwen3-next-80b-a3b-instruct.toml @@ -3,9 +3,7 @@ name = "Qwen3 Next 80B A3B Instruct" description = "Qwen instruction model for multilingual chat, reasoning, and tool use" release_date = "2025-09-10" last_updated = "2025-09-10" -reasoning = true structured_output = true -reasoning_options = [] [cost] input = 0.15 diff --git a/sync.md b/sync.md index c8b61a1f489..7fbfada24af 100644 --- a/sync.md +++ b/sync.md @@ -175,6 +175,7 @@ CrossModel is implemented in `packages/core/src/sync/providers/crossmodel.ts`. - The endpoint supplies current pricing, modalities, features, and limits; authored audio/reasoning prices and cache prices missing from its response remain intact. Tier-specific optional prices are retained only for an identical context threshold. Explicit zero input/output prices with no pricing or tiers mean a free model. - Existing authored descriptions and provider-specific reasoning controls remain curated: the API can truncate descriptions and does not describe each model's reasoning wire controls. New models lacking verified lab metadata, usable prices, or reasoning controls are skipped and tracked through deduped missing-model issues in GitHub Actions. - New reasoning models use an exact-ID list of live-tested `thinking.type = enabled|disabled` controls. The same field is ignored by some Novita routes (including GLM-5.3 Flash and DeepSeek V4 Pro 0813), so it must not be inferred for an entire lab or from a `reasoning` feature flag. Unknown effort levels are not published. +- Missing-model issues are limited to chat-completion catalog rows with a usable context window; image, embedding, and other non-chat rows are intentionally ignored. - An empty response is rejected; a run removing more than half the existing catalog fails before any files are written. Smaller removals follow the complete remote catalog. ## OpenRouter Notes From a6acdf74626c763390d2e190f08b707cce190779 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 15:07:10 +0800 Subject: [PATCH 13/17] fix(novita-ai): resolve reasoning review findings --- packages/core/src/sync/providers/novita-ai.ts | 8 ++++++++ .../models/deepseek/deepseek-v3.1-terminus.toml | 2 ++ providers/novita-ai/models/deepseek/deepseek-v3.1.toml | 2 ++ .../novita-ai/models/deepseek/deepseek-v3.2-exp.toml | 2 ++ providers/novita-ai/models/deepseek/deepseek-v3.2.toml | 2 ++ .../novita-ai/models/deepseek/deepseek-v4-flash.toml | 6 ++---- providers/novita-ai/models/deepseek/deepseek-v4-pro.toml | 6 ++---- .../novita-ai/models/deepseek/deepseek-v4.1-flash.toml | 4 +--- .../novita-ai/models/google/gemma-4-26b-a4b-it.toml | 2 ++ providers/novita-ai/models/google/gemma-4-31b-it.toml | 2 ++ providers/novita-ai/models/moonshotai/kimi-k2.5.toml | 2 ++ providers/novita-ai/models/moonshotai/kimi-k2.6.toml | 2 ++ .../novita-ai/models/moonshotai/kimi-k2.7-code.toml | 2 ++ providers/novita-ai/models/moonshotai/kimi-k3.toml | 2 ++ providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml | 2 ++ providers/novita-ai/models/qwen/qwen3.5-27b.toml | 2 ++ providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml | 2 ++ providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml | 2 ++ providers/novita-ai/models/qwen/qwen3.7-max.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.5-air.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.5v.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.6.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.6v.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.7-flash.toml | 2 ++ providers/novita-ai/models/zai-org/glm-4.7.toml | 2 ++ providers/novita-ai/models/zai-org/glm-5.1.toml | 2 ++ providers/novita-ai/models/zai-org/glm-5.toml | 2 ++ providers/novita-ai/provider.toml | 9 ++++----- sync.md | 1 + 29 files changed, 64 insertions(+), 16 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 966ad04d73e..a8308d7fa2f 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -32,6 +32,12 @@ const VERIFIED_THINKING_TOGGLE = new Set([ const VERIFIED_NON_REASONING = new Set([ "qwen/qwen3-next-80b-a3b-instruct", ]); +// Novita accepts the thinking toggle for these routes, but no effort ladder +// was verified; do not preserve an inherited guessed ladder from older files. +const VERIFIED_TOGGLE_ONLY = new Set([ + "deepseek/deepseek-v4-flash", + "deepseek/deepseek-v4-pro", +]); const VERIFIED_TOGGLE_HEADER = "# Toggle: thinking.type = enabled|disabled\n# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content.\n"; const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); const Pricing = z.object({ @@ -164,6 +170,7 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi // DeepSeek R1 is fixed-reasoning on Novita, as with its already curated R1 variants. const reasoningOptions = VERIFIED_NON_REASONING.has(model.id) ? undefined : VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] + : VERIFIED_TOGGLE_ONLY.has(model.id) ? [{ type: "toggle" as const }] : existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; @@ -224,6 +231,7 @@ export const novitaAi = { // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, deleteMissing: true, + authoritativeHeaders: true, trackMissingModels: true, maxMissingFraction: 0.5, missingModelID(model) { diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml b/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml index b1c67cb6579..f8d7ae64997 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.1-terminus.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. name = "DeepSeek V3.1 Terminus" description = "DeepSeek chat model for instruction following, coding, and analysis" family = "deepseek" diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.1.toml b/providers/novita-ai/models/deepseek/deepseek-v3.1.toml index 9af285358a0..1c29ea12e6b 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.1.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.1.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v3.1" name = "DeepSeek V3.1" description = "DeepSeek chat model for instruction following, coding, and analysis" diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml index 6922812888c..875ebbfa23e 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.2-exp.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. name = "DeepSeek V3.2 Exp" description = "DeepSeek chat model for instruction following, coding, and analysis" release_date = "2025-09-29" diff --git a/providers/novita-ai/models/deepseek/deepseek-v3.2.toml b/providers/novita-ai/models/deepseek/deepseek-v3.2.toml index 6abf57fe73f..8ce2ea00f9f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v3.2.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v3.2.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v3.2" description = "DeepSeek chat model for instruction following, coding, and analysis" diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml index 4b4f07e0ff5..1812d70c601 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash" description = "DeepSeek-V4-Flash is a lightweight model meticulously designed by DeepSeek to deliver the ultimate combination of lightning-fast response times and unmatched cost-effectiveness. Engineered with fewer parameters and significantly lower activation overhead, V4-Flash provides an exceptionally fast and economical API service. At its core, V4-Flash demonstrates outstanding reasoning capabilities that closely rival the V4-Pro model. While featuring a slightly streamlined repository of world knowledge, it remains highly capable of satisfying the demands of most application scenarios. In Agentic applications, V4-Flash performs on par with the Pro version when handling standard and fundamental tasks. As the premier choice for developers prioritizing high concurrency, low latency, and cost efficiency, DeepSeek-V4-Flash serves as the optimal solution for deploying large-scale, high-frequency, and lightweight AI workloads." @@ -7,10 +9,6 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" -[[reasoning_options]] -type = "effort" -values = ["minimal", "low", "medium", "high", "xhigh"] - [cost] input = 0.14 output = 0.28 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml index 7ff7b56005d..d4faac6c73f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-pro" description = "DeepSeek-V4-Pro is the next-generation flagship open-source large language model developed by DeepSeek, delivering comprehensive performance that rivals the world's premier closed-source models. Compared to its predecessor, V4-Pro achieves a breakthrough evolution in Agentic capabilities. It firmly holds the top position among open-source models in Agentic Coding, providing a high-quality, end-to-end code delivery experience that surpasses mainstream industry benchmarks (such as Sonnet 4.5). Furthermore, the model not only boasts an expansive repository of world knowledge that leads the open-source community, but it also demonstrates ultimate logical reasoning prowess in highly demanding evaluations—including mathematics, STEM, and competitive programming. In these rigorous domains, V4-Pro outperforms all publicly evaluated open-source models and matches the capabilities of global closed-source giants. As the ideal foundational model for building complex agentic workflows, professional-grade software developm" @@ -7,10 +9,6 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" -[[reasoning_options]] -type = "effort" -values = ["low", "medium", "high", "xhigh"] - [cost] input = 1.6 output = 3.2 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml index b0518c72f69..44d4e0a8d1f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml @@ -1,7 +1,5 @@ # Toggle: thinking.type = enabled|disabled -# Verified on 2026-09-17 with Novita chat/completions: enabled returns -# reasoning_content/reasoning_tokens; disabled returns neither. -# Pricing and limits: https://novita.ai/models/model-detail/deepseek-deepseek-v4.1-flash +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4.1-flash" [interleaved] diff --git a/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml b/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml index 76e4506c1e8..220dd129017 100644 --- a/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml +++ b/providers/novita-ai/models/google/gemma-4-26b-a4b-it.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "google/gemma-4-26b-a4b-it" name = "Gemma 4 26B A4B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" diff --git a/providers/novita-ai/models/google/gemma-4-31b-it.toml b/providers/novita-ai/models/google/gemma-4-31b-it.toml index 859043ec420..15aa93c3f73 100644 --- a/providers/novita-ai/models/google/gemma-4-31b-it.toml +++ b/providers/novita-ai/models/google/gemma-4-31b-it.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "google/gemma-4-31b-it" name = "Gemma 4 31B" description = "Open Gemma instruction model for efficient chat and self-hosted deployments" diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.5.toml b/providers/novita-ai/models/moonshotai/kimi-k2.5.toml index 42e46485b00..33e5fa9a966 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.5.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.5.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "moonshotai/kimi-k2.5" description = "Kimi multimodal agent model for visual understanding, coding, and planning" release_date = "2026-01-27" diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.6.toml b/providers/novita-ai/models/moonshotai/kimi-k2.6.toml index 43dfa3a1c27..698de8d6e66 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.6.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.6.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "moonshotai/kimi-k2.6" description = "Kimi K2.6 is an open-source, native multimodal agentic model that significantly advances practical capabilities in long-horizon coding, coding-driven design, and swarm-based task orchestration. It robustly executes complex, end-to-end development tasks across multiple programming languages and domains, seamlessly transforming simple prompts and visual inputs into production-ready, aesthetically precise interfaces and full-stack workflows. Uniquely engineered for high scalability, K2.6 can horizontally orchestrate up to 300 domain-specialized sub-agents through 4,000 coordinated steps, dynamically decomposing intricate tasks to deliver diverse end-to-end outputs—from documents and spreadsheets to fully functional websites—in a single autonomous run. Furthermore, its proactive execution capabilities empower persistent, 24/7 background agents to manage schedules, deploy code, and orchestrate cross-platform operations entirely without human oversight, establishing it as a premier foundational model for next-gener" diff --git a/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml b/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml index eeafdb61965..1be4368e845 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k2.7-code.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "moonshotai/kimi-k2.7-code" description = "Kimi K2.7 Code is MoonshotAI's strongest coding & agentic model — a 1T-parameter MoE (32B activated) , 256K context and interleaved thinking with multi-step tool calling. It delivers major gains on long-horizon coding tasks while cutting thinking-token usage by ~30% vs K2.6, and accepts text, image and video inputs for vision-driven development workflows." diff --git a/providers/novita-ai/models/moonshotai/kimi-k3.toml b/providers/novita-ai/models/moonshotai/kimi-k3.toml index 47646f26271..a582218b457 100644 --- a/providers/novita-ai/models/moonshotai/kimi-k3.toml +++ b/providers/novita-ai/models/moonshotai/kimi-k3.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "moonshotai/kimi-k3" description = "Kimi K3 is Kimi’s most capable model to date, with 2.8 trillion parameters. Built on Kimi Delta Attention, a hybrid linear attention mechanism, and Attention Residuals, it offers native visual understanding and a 1M-token context window for frontier intelligence scenarios such as software engineering, knowledge work, and deep reasoning." diff --git a/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml b/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml index 671ce23baaf..aef34bf0cfb 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-122b-a10b.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.5-122b-a10b" name = "Qwen3.5 122B A10B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" diff --git a/providers/novita-ai/models/qwen/qwen3.5-27b.toml b/providers/novita-ai/models/qwen/qwen3.5-27b.toml index b463ab43a8b..d39114e6e99 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-27b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-27b.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.5-27b" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" release_date = "2026-02-26" diff --git a/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml b/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml index f9517d02f8e..766bbe5ee7d 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-35b-a3b.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.5-35b-a3b" name = "Qwen3.5 35B A3B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" diff --git a/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml b/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml index 5afda7de2a4..8428245f736 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-397b-a17b.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.5-397b-a17b" name = "Qwen3.5 397B A17B" description = "Qwen vision-language model for visual reasoning, documents, and agent tasks" diff --git a/providers/novita-ai/models/qwen/qwen3.7-max.toml b/providers/novita-ai/models/qwen/qwen3.7-max.toml index 19857d4c77c..2845a271b18 100644 --- a/providers/novita-ai/models/qwen/qwen3.7-max.toml +++ b/providers/novita-ai/models/qwen/qwen3.7-max.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.7-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows" last_updated = "2026-05-27" diff --git a/providers/novita-ai/models/zai-org/glm-4.5-air.toml b/providers/novita-ai/models/zai-org/glm-4.5-air.toml index 3e3363de82b..0184b3b8528 100644 --- a/providers/novita-ai/models/zai-org/glm-4.5-air.toml +++ b/providers/novita-ai/models/zai-org/glm-4.5-air.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.5-air" name = "GLM 4.5 Air" description = "Efficient GLM model for fast reasoning, coding, and agent workflows" diff --git a/providers/novita-ai/models/zai-org/glm-4.5v.toml b/providers/novita-ai/models/zai-org/glm-4.5v.toml index 929071c1119..8ff04af7bcd 100644 --- a/providers/novita-ai/models/zai-org/glm-4.5v.toml +++ b/providers/novita-ai/models/zai-org/glm-4.5v.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.5v" name = "GLM 4.5V" description = "GLM vision model for visual reasoning, documents, and multimodal agents" diff --git a/providers/novita-ai/models/zai-org/glm-4.6.toml b/providers/novita-ai/models/zai-org/glm-4.6.toml index 38cbbf95b21..25c53108d02 100644 --- a/providers/novita-ai/models/zai-org/glm-4.6.toml +++ b/providers/novita-ai/models/zai-org/glm-4.6.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.6" name = "GLM 4.6" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" diff --git a/providers/novita-ai/models/zai-org/glm-4.6v.toml b/providers/novita-ai/models/zai-org/glm-4.6v.toml index 010c72f7d6c..08ffd62a76e 100644 --- a/providers/novita-ai/models/zai-org/glm-4.6v.toml +++ b/providers/novita-ai/models/zai-org/glm-4.6v.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.6v" name = "GLM 4.6V" description = "GLM vision model for visual reasoning, documents, and multimodal agents" diff --git a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml index b6b76b98fe1..10963bbe0ad 100644 --- a/providers/novita-ai/models/zai-org/glm-4.7-flash.toml +++ b/providers/novita-ai/models/zai-org/glm-4.7-flash.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.7-flash" name = "GLM 4.7 Flash" description = "Efficient GLM model for fast reasoning, coding, and agent workflows" diff --git a/providers/novita-ai/models/zai-org/glm-4.7.toml b/providers/novita-ai/models/zai-org/glm-4.7.toml index 9d14eb4f926..db6ad2d5267 100644 --- a/providers/novita-ai/models/zai-org/glm-4.7.toml +++ b/providers/novita-ai/models/zai-org/glm-4.7.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-4.7" name = "GLM 4.7" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" diff --git a/providers/novita-ai/models/zai-org/glm-5.1.toml b/providers/novita-ai/models/zai-org/glm-5.1.toml index 72b58f0d69e..ddb976fbd6b 100644 --- a/providers/novita-ai/models/zai-org/glm-5.1.toml +++ b/providers/novita-ai/models/zai-org/glm-5.1.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-5.1" name = "GLM 5.1" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" diff --git a/providers/novita-ai/models/zai-org/glm-5.toml b/providers/novita-ai/models/zai-org/glm-5.toml index e0eb6b2b390..1b4f5e44aef 100644 --- a/providers/novita-ai/models/zai-org/glm-5.toml +++ b/providers/novita-ai/models/zai-org/glm-5.toml @@ -1,3 +1,5 @@ +# Toggle: thinking.type = enabled|disabled +# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "zhipuai/glm-5" name = "GLM 5" description = "Flagship GLM model for hybrid reasoning, coding, and agentic engineering" diff --git a/providers/novita-ai/provider.toml b/providers/novita-ai/provider.toml index 2af00eda81b..32468504059 100644 --- a/providers/novita-ai/provider.toml +++ b/providers/novita-ai/provider.toml @@ -1,11 +1,10 @@ name = "Novita AI" env = ["NOVITA_API_KEY"] npm = "@ai-sdk/openai-compatible" -# Raw HTTP reasoning controls (sources accessed 2026-06-25): -# POST `/openai/v1/chat/completions` accepts top-level `enable_thinking = -# true|false` (default true), but documents it only for zai-org/glm-4.5 and -# deepseek/deepseek-v3.1, -v3.1-terminus, and -v3.2-exp. `separate_reasoning` -# is a distinct boolean documented only for deepseek/deepseek-r1-turbo. +# Raw HTTP reasoning controls (verified 2026-09-17): +# Newer Novita routes accept `thinking.type = enabled|disabled`; the model +# headers identify routes tested with that control. Legacy routes may still +# use `enable_thinking` or `separate_reasoning` as documented by Novita. # https://novita.ai/docs/api-reference/model-apis-llm-create-chat-completion doc = "https://novita.ai/docs/guides/introduction" api = "https://api.novita.ai/openai" diff --git a/sync.md b/sync.md index 7fbfada24af..a9f81b47f0e 100644 --- a/sync.md +++ b/sync.md @@ -176,6 +176,7 @@ CrossModel is implemented in `packages/core/src/sync/providers/crossmodel.ts`. - Existing authored descriptions and provider-specific reasoning controls remain curated: the API can truncate descriptions and does not describe each model's reasoning wire controls. New models lacking verified lab metadata, usable prices, or reasoning controls are skipped and tracked through deduped missing-model issues in GitHub Actions. - New reasoning models use an exact-ID list of live-tested `thinking.type = enabled|disabled` controls. The same field is ignored by some Novita routes (including GLM-5.3 Flash and DeepSeek V4 Pro 0813), so it must not be inferred for an entire lab or from a `reasoning` feature flag. Unknown effort levels are not published. - Missing-model issues are limited to chat-completion catalog rows with a usable context window; image, embedding, and other non-chat rows are intentionally ignored. +- `/openai/v1/models` is treated as Novita's complete public model catalog: the endpoint is the source used by the model detail catalog and returns the served public rows, so absent local rows are removed after the shrink guard passes. - An empty response is rejected; a run removing more than half the existing catalog fails before any files are written. Smaller removals follow the complete remote catalog. ## OpenRouter Notes From ffaa9451e66ab8656192c460f931df1a32e48865 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 15:56:08 +0800 Subject: [PATCH 14/17] fix(novita-ai): address latest review findings --- packages/core/src/sync/providers/novita-ai.ts | 1 - providers/novita-ai/models/minimax/minimax-m2.1.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index a8308d7fa2f..8de033506b3 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -231,7 +231,6 @@ export const novitaAi = { // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, deleteMissing: true, - authoritativeHeaders: true, trackMissingModels: true, maxMissingFraction: 0.5, missingModelID(model) { diff --git a/providers/novita-ai/models/minimax/minimax-m2.1.toml b/providers/novita-ai/models/minimax/minimax-m2.1.toml index d846a31adc5..52ee640da74 100644 --- a/providers/novita-ai/models/minimax/minimax-m2.1.toml +++ b/providers/novita-ai/models/minimax/minimax-m2.1.toml @@ -1,8 +1,8 @@ base_model = "minimax/MiniMax-M2.1" name = "MiniMax M2.1" description = "MiniMax model for chat, coding, office work, and agentic tasks" -reasoning = false structured_output = true +reasoning_options = [] [interleaved] field = "reasoning_content" From 53d870174657b6ab7b9007d6dc4147488099a3e2 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 16:33:43 +0800 Subject: [PATCH 15/17] fix(novita-ai): align sync controls with live API behavior --- packages/core/src/sync/providers/novita-ai.ts | 29 ++++++++++++++----- packages/core/test/novita-ai.test.ts | 27 +++++++++-------- .../deepseek/deepseek-v4-flash-0731.toml | 10 +++++++ .../deepseek-v4-flash-vision-exp.toml | 5 ++++ .../models/deepseek/deepseek-v4-flash.toml | 5 ++++ .../models/deepseek/deepseek-v4-pro.toml | 5 ++++ .../models/deepseek/deepseek-v4.1-flash.toml | 5 ++++ .../novita-ai/models/qwen/qwen3-max.toml | 4 +++ .../novita-ai/models/qwen/qwen3.5-plus.toml | 4 +++ sync.md | 8 ++--- 10 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 8de033506b3..94d888c7633 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -35,10 +35,19 @@ const VERIFIED_NON_REASONING = new Set([ // Novita accepts the thinking toggle for these routes, but no effort ladder // was verified; do not preserve an inherited guessed ladder from older files. const VERIFIED_TOGGLE_ONLY = new Set([ - "deepseek/deepseek-v4-flash", - "deepseek/deepseek-v4-pro", + "deepseek/deepseek-v4-flash-vision-exp", ]); const VERIFIED_TOGGLE_HEADER = "# Toggle: thinking.type = enabled|disabled\n# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content.\n"; +const VERIFIED_BUDGET_TOGGLE = new Set([ + "qwen/qwen3.5-plus", + "qwen/qwen3.6-27b", + "qwen/qwen3.6-35b-a3b", + "qwen/qwen3.6-plus", + "qwen/qwen3.8-27b", + "qwen/qwen3.8-flash", + "qwen/qwen3.8-max", + "qwen/qwen3-max", +]); const Price = z.object({ price_per_m_decimal: z.string().optional() }).passthrough(); const Pricing = z.object({ prompt: Price.optional(), @@ -153,9 +162,9 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const output = modalities(model.output_modalities, resolved?.modalities?.output) ?? ["text"]; const features = model.features === undefined ? undefined : new Set(model.features); const featureValue = (feature: string, fallback: boolean | undefined) => - features === undefined || features.size === 0 + features === undefined || features.size === 0 || !features.has(feature) ? fallback ?? false - : features.has(feature) || fallback === true; + : true; const reasoning = VERIFIED_NON_REASONING.has(model.id) ? false : featureValue("reasoning", resolved?.reasoning); const toolCall = featureValue("function-calling", resolved?.tool_call); const structuredOutput = featureValue("structured-outputs", resolved?.structured_output); @@ -169,8 +178,11 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi const description = model.id === "zai-org/glm-5.3" ? undefined : model.description; // DeepSeek R1 is fixed-reasoning on Novita, as with its already curated R1 variants. const reasoningOptions = VERIFIED_NON_REASONING.has(model.id) ? undefined - : VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] + : VERIFIED_BUDGET_TOGGLE.has(model.id) ? [{ type: "toggle" as const }, { type: "budget_tokens" as const }] + : VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] : VERIFIED_TOGGLE_ONLY.has(model.id) ? [{ type: "toggle" as const }] + : model.id === "deepseek/deepseek-v4-pro" ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["high", "max"] }] + : ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"].includes(model.id) ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["low", "high", "max"] }] : existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; @@ -230,13 +242,16 @@ export const novitaAi = { modelsDir: "providers/novita-ai/models", // The endpoint exposes the metadata needed to author new provider models. skipCreates: false, - deleteMissing: true, + // The authenticated inventory may be account- or tier-scoped; never delete + // locally curated models solely because a key cannot see them. + deleteMissing: false, trackMissingModels: true, maxMissingFraction: 0.5, missingModelID(model) { // The endpoint also exposes image, embedding, and other non-chat rows. // Only chat models are candidates for a provider catalog TOML/issue. if (model.model_type !== "chat" || !model.endpoints?.includes("chat/completions") || model.context_size === 0) return undefined; + if (model.pricing === undefined && model.input_token_price_per_m === undefined && model.output_token_price_per_m === undefined) return undefined; return model.id; }, sourceID(model) { @@ -259,7 +274,7 @@ export const novitaAi = { id: model.id, model: translated, header: ("reasoning_options" in translated && translated.reasoning_options?.some((option) => option.type === "toggle")) - ? VERIFIED_TOGGLE_HEADER + ? `${VERIFIED_TOGGLE_HEADER}${VERIFIED_BUDGET_TOGGLE.has(model.id) ? "# Budget: thinking_budget (integer reasoning tokens)\n" : ""}` : undefined, }; }, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index 1a6be9a9489..d1c0d75cc0f 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -204,7 +204,7 @@ test("Novita AI sync reuses a verified lab alias and fixed R1 controls", () => { test("Novita AI sync updates V4.1 Flash prices while retaining its verified toggle", () => { const authored: ExistingModel = { base_model: "deepseek/deepseek-v4.1-flash", - reasoning_options: [{ type: "toggle" }], + reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }], interleaved: { field: "reasoning_content" }, cost: { input: 1, output: 2 }, }; @@ -223,7 +223,7 @@ test("Novita AI sync updates V4.1 Flash prices while retaining its verified togg }), { authored: () => authored, existing: () => authored }); expect(translated?.model).toMatchObject({ base_model: authored.base_model, - reasoning_options: [{ type: "toggle" }], + reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }], interleaved: { field: "reasoning_content" }, cost: { input: 0.3, output: 1.2, cache_read: 0.006 }, limit: { context: 1_048_576, output: 393_216 }, @@ -236,7 +236,10 @@ test("Novita AI sync creates only explicitly verified new reasoners", () => { const pricing = { prompt: { price_per_m_decimal: "0.15" }, completion: { price_per_m_decimal: "0.5" } }; for (const id of ["qwen/qwen3.8-flash", "minimax/minimax-m3", "zai-org/glm-5.3", "deepseek/deepseek-v4-flash-0731"]) { const translated = novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context); - expect(translated?.model).toMatchObject({ reasoning_options: [{ type: "toggle" }], interleaved: { field: "reasoning_content" } }); + expect(translated?.model).toMatchObject({ + reasoning_options: id.startsWith("qwen/") ? [{ type: "toggle" }, { type: "budget_tokens" }] : [{ type: "toggle" }], + interleaved: { field: "reasoning_content" }, + }); expect(translated?.header).toContain("thinking.type = enabled|disabled"); if (id === "zai-org/glm-5.3") expect(translated?.model).not.toHaveProperty("description"); } @@ -262,7 +265,7 @@ test("Novita AI sync treats verified Qwen reasoning behavior per model", () => { const price = { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } }; const context = { authored: () => undefined, existing: () => undefined }; expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-max", features: ["reasoning"], pricing: price }), context)?.model) - .toMatchObject({ reasoning: true, reasoning_options: [{ type: "toggle" }] }); + .toMatchObject({ reasoning: true, reasoning_options: [{ type: "toggle" }, { type: "budget_tokens" }] }); expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-next-80b-a3b-instruct", features: ["reasoning"], pricing: price }), context)?.model) .not.toHaveProperty("reasoning_options"); expect(novitaAi.translateModel(novitaAiModel({ id: "qwen/qwen3-next-80b-a3b-instruct", features: ["reasoning"], pricing: price }), context)?.model) @@ -319,7 +322,7 @@ test("Novita AI sync updates existing inline model capabilities", () => { }); }); -test("Novita AI sync removes local models absent from API response", async () => { +test("Novita AI sync retains local models absent from API response", async () => { const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-ai-")); const modelsDir = path.join(dir, "providers", "novita-ai", "models"); await mkdir(modelsDir, { recursive: true }); @@ -367,14 +370,14 @@ test("Novita AI sync removes local models absent from API response", async () => }; }, }); - expect(result.deleted).toBe(1); - expect(await Bun.file(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml")).exists()).toBe(false); + expect(result.deleted).toBe(0); + expect(await Bun.file(path.join(modelsDir, "deepseek", "deepseek-v3.2.toml")).exists()).toBe(true); } finally { await rm(dir, { recursive: true, force: true }); } }); -test("Novita AI sync refuses a partial response before updating any files", async () => { +test("Novita AI sync updates visible models without deleting unseen files", async () => { const dir = await mkdtemp(path.join(tmpdir(), "sync-novita-guard-")); const modelsDir = path.join(dir, "providers", "novita-ai", "models"); const file = path.join(modelsDir, "novita", "custom.toml"); @@ -388,8 +391,8 @@ test("Novita AI sync refuses a partial response before updating any files", asyn await expect(syncProvider({ ...novitaAi, modelsDir, maxMissingFraction: 0.49, async fetchModels() { return { data: [novitaAiModel({ id: "novita/custom", features: [], pricing: { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } } })] }; }, - })).rejects.toThrow("would delete 1/2 existing models"); - expect(await Bun.file(file).text()).toBe(content); + })).resolves.toMatchObject({ deleted: 0 }); + expect(await Bun.file(file).text()).not.toBe(content); expect(await Bun.file(other).text()).toBe(content); } finally { await rm(dir, { recursive: true, force: true }); @@ -411,7 +414,7 @@ test("Novita AI sync keeps local files when translation skips an existing remote }, { dryRun: true, openIssues: true }); expect(result.deleted).toBe(0); expect(result.notices.join(" ")).toContain("novita/custom"); - expect(result.notices.join(" ")).toContain("Would open GitHub issue"); + expect(result.notices.join(" ")).toContain("Novita models needing lab metadata"); expect(await Bun.file(file).text()).toBe(content); } finally { await rm(dir, { recursive: true, force: true }); @@ -424,7 +427,7 @@ test("Novita AI sync tracks remote-only IDs", () => { expect(novitaAi.sourceID?.(novitaAiModel())).toBe("deepseek/deepseek-v3.2"); expect(novitaAi.sourceID?.(novitaAiModel({ id: "novita/new-model" }))).toBe("novita/new-model"); expect(novitaAi.trackMissingModels).toBe(true); - expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/new-model", model_type: "chat", endpoints: ["chat/completions"] }))).toBe("novita/new-model"); + expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/new-model", model_type: "chat", endpoints: ["chat/completions"] }))).toBeUndefined(); expect(novitaAi.missingModelID?.(novitaAiModel({ id: "novita/image", model_type: "image", endpoints: ["images/generations"] }))).toBeUndefined(); }); diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml index 3e83d5bf90d..da7bdf06b6f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml @@ -1,4 +1,6 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = low|high|max +# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash-0731" description = "DeepSeek V4 Flash 0731 is a sparse mixture-of-experts model from DeepSeek, with 13B active parameters out of 284B total. This re-post-trained revision is suited for coding, reasoning, and agent workflows." @@ -9,6 +11,14 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + [cost] input = 0.44 output = 1.32 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml index 32c2b130742..baa7a475c53 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash-vision-exp" description = "DeepSeek V4 Flash Vision Exp is an experimental vision-enabled version of DeepSeek V4 Flash 0731(opens in new tab) from DeepSeek, adding image understanding while matching the base model on text capabilities including agents, reasoning, and world knowledge. It is a sparse mixture-of-experts model with 13B active parameters out of 284B total.\n\nIt is suited for document and chart understanding, visual question answering, and multimodal agent workflows that interleave text and images." @@ -9,6 +10,10 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + [cost] input = 0.44 output = 1.32 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml index 1812d70c601..ff310955e54 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash" description = "DeepSeek-V4-Flash is a lightweight model meticulously designed by DeepSeek to deliver the ultimate combination of lightning-fast response times and unmatched cost-effectiveness. Engineered with fewer parameters and significantly lower activation overhead, V4-Flash provides an exceptionally fast and economical API service. At its core, V4-Flash demonstrates outstanding reasoning capabilities that closely rival the V4-Pro model. While featuring a slightly streamlined repository of world knowledge, it remains highly capable of satisfying the demands of most application scenarios. In Agentic applications, V4-Flash performs on par with the Pro version when handling standard and fundamental tasks. As the premier choice for developers prioritizing high concurrency, low latency, and cost efficiency, DeepSeek-V4-Flash serves as the optimal solution for deploying large-scale, high-frequency, and lightweight AI workloads." @@ -9,6 +10,10 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + [cost] input = 0.14 output = 0.28 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml index d4faac6c73f..5739d1fa55d 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-pro.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-pro" description = "DeepSeek-V4-Pro is the next-generation flagship open-source large language model developed by DeepSeek, delivering comprehensive performance that rivals the world's premier closed-source models. Compared to its predecessor, V4-Pro achieves a breakthrough evolution in Agentic capabilities. It firmly holds the top position among open-source models in Agentic Coding, providing a high-quality, end-to-end code delivery experience that surpasses mainstream industry benchmarks (such as Sonnet 4.5). Furthermore, the model not only boasts an expansive repository of world knowledge that leads the open-source community, but it also demonstrates ultimate logical reasoning prowess in highly demanding evaluations—including mathematics, STEM, and competitive programming. In these rigorous domains, V4-Pro outperforms all publicly evaluated open-source models and matches the capabilities of global closed-source giants. As the ideal foundational model for building complex agentic workflows, professional-grade software developm" @@ -9,6 +10,10 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["high", "max"] + [cost] input = 1.6 output = 3.2 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml index 44d4e0a8d1f..2e19830eac5 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4.1-flash.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4.1-flash" @@ -8,6 +9,10 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + [cost] input = 0.3 output = 1.2 diff --git a/providers/novita-ai/models/qwen/qwen3-max.toml b/providers/novita-ai/models/qwen/qwen3-max.toml index f1eedc42d82..28548d19f25 100644 --- a/providers/novita-ai/models/qwen/qwen3-max.toml +++ b/providers/novita-ai/models/qwen/qwen3-max.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows" @@ -13,6 +14,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.845 output = 3.38 diff --git a/providers/novita-ai/models/qwen/qwen3.5-plus.toml b/providers/novita-ai/models/qwen/qwen3.5-plus.toml index 37b217ebee1..1e63a9d2933 100644 --- a/providers/novita-ai/models/qwen/qwen3.5-plus.toml +++ b/providers/novita-ai/models/qwen/qwen3.5-plus.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.5-plus" name = "Qwen3.5-Plus" @@ -12,6 +13,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.4 output = 2.4 diff --git a/sync.md b/sync.md index a9f81b47f0e..943c6120b31 100644 --- a/sync.md +++ b/sync.md @@ -171,13 +171,13 @@ CrossModel is implemented in `packages/core/src/sync/providers/crossmodel.ts`. ## Novita AI Notes -- Novita AI uses the authenticated `https://api.novita.ai/openai/v1/models` endpoint as the complete served-model catalog. Set `NOVITA_API_KEY` locally or in the hourly workflow. +- Novita AI uses the authenticated `https://api.novita.ai/openai/v1/models` endpoint as an account-visible served-model catalog. Set `NOVITA_API_KEY` locally or in the hourly workflow; local entries are retained when a key does not expose them. - The endpoint supplies current pricing, modalities, features, and limits; authored audio/reasoning prices and cache prices missing from its response remain intact. Tier-specific optional prices are retained only for an identical context threshold. Explicit zero input/output prices with no pricing or tiers mean a free model. - Existing authored descriptions and provider-specific reasoning controls remain curated: the API can truncate descriptions and does not describe each model's reasoning wire controls. New models lacking verified lab metadata, usable prices, or reasoning controls are skipped and tracked through deduped missing-model issues in GitHub Actions. -- New reasoning models use an exact-ID list of live-tested `thinking.type = enabled|disabled` controls. The same field is ignored by some Novita routes (including GLM-5.3 Flash and DeepSeek V4 Pro 0813), so it must not be inferred for an entire lab or from a `reasoning` feature flag. Unknown effort levels are not published. +- New reasoning models use an exact-ID list of live-tested `thinking.type = enabled|disabled` controls. Qwen routes with a tested `thinking_budget` expose `budget_tokens`; DeepSeek V4 routes with a tested `reasoning_effort` expose only the lab/peer-supported levels. The same field is ignored by some Novita routes (including GLM-5.3 Flash and DeepSeek V4 Pro 0813), so it must not be inferred for an entire lab or from a `reasoning` feature flag. Unknown controls are not published. - Missing-model issues are limited to chat-completion catalog rows with a usable context window; image, embedding, and other non-chat rows are intentionally ignored. -- `/openai/v1/models` is treated as Novita's complete public model catalog: the endpoint is the source used by the model detail catalog and returns the served public rows, so absent local rows are removed after the shrink guard passes. -- An empty response is rejected; a run removing more than half the existing catalog fails before any files are written. Smaller removals follow the complete remote catalog. +- `/openai/v1/models` is not treated as authoritative for deletion because visibility can be account- or tier-scoped. The sync updates visible rows but does not remove absent local files. +- An empty response is rejected; missing-model notices are limited to priced chat-completion rows that need manual lab metadata or verified controls. Image, embedding, unpriced, and other non-chat rows are intentionally ignored. ## OpenRouter Notes From 65525435e26614fc16ad57ec8dda080cd5baeba5 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 18:40:03 +0800 Subject: [PATCH 16/17] fix(novita-ai): preserve verified reasoning controls --- packages/core/src/sync/providers/novita-ai.ts | 12 +----------- packages/core/test/novita-ai.test.ts | 19 ++++++++++++++++++- .../deepseek/deepseek-v4-flash-0731.toml | 5 ----- .../deepseek-v4-flash-vision-exp.toml | 4 ---- .../novita-ai/models/qwen/qwen3.6-27b.toml | 4 ++++ .../models/qwen/qwen3.6-35b-a3b.toml | 4 ++++ .../novita-ai/models/qwen/qwen3.6-plus.toml | 4 ++++ .../novita-ai/models/qwen/qwen3.8-27b.toml | 4 ++++ .../novita-ai/models/qwen/qwen3.8-flash.toml | 4 ++++ .../novita-ai/models/qwen/qwen3.8-max.toml | 4 ++++ 10 files changed, 43 insertions(+), 21 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index 94d888c7633..fb31483b104 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -11,19 +11,9 @@ const BASE_MODEL_ALIASES: Record = { // Verified per model with Novita chat/completions: disabling thinking removes // reasoning_content, while enabling it returns reasoning_content. const VERIFIED_THINKING_TOGGLE = new Set([ - "deepseek/deepseek-v4-flash-0731", - "deepseek/deepseek-v4-flash-vision-exp", "inclusionai/ling-3.0-flash-fin", "minimax/minimax-m3", "nvidia/nemotron-3-nano-30b-a3b", - "qwen/qwen3.5-plus", - "qwen/qwen3.6-27b", - "qwen/qwen3.6-35b-a3b", - "qwen/qwen3.6-plus", - "qwen/qwen3.8-27b", - "qwen/qwen3.8-flash", - "qwen/qwen3.8-max", - "qwen/qwen3-max", "tencent/hy3", "zai-org/glm-5-turbo", "zai-org/glm-5.3", @@ -184,7 +174,7 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi : model.id === "deepseek/deepseek-v4-pro" ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["high", "max"] }] : ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"].includes(model.id) ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["low", "high", "max"] }] : existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); - const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) ? { field: "reasoning_content" as const } : undefined); + const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) || VERIFIED_BUDGET_TOGGLE.has(model.id) || VERIFIED_TOGGLE_ONLY.has(model.id) || model.id === "deepseek/deepseek-v4-pro" || ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"].includes(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index d1c0d75cc0f..a5e138d6a54 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -237,7 +237,11 @@ test("Novita AI sync creates only explicitly verified new reasoners", () => { for (const id of ["qwen/qwen3.8-flash", "minimax/minimax-m3", "zai-org/glm-5.3", "deepseek/deepseek-v4-flash-0731"]) { const translated = novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context); expect(translated?.model).toMatchObject({ - reasoning_options: id.startsWith("qwen/") ? [{ type: "toggle" }, { type: "budget_tokens" }] : [{ type: "toggle" }], + reasoning_options: id.startsWith("qwen/") + ? [{ type: "toggle" }, { type: "budget_tokens" }] + : id === "deepseek/deepseek-v4-flash-0731" + ? [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }] + : [{ type: "toggle" }], interleaved: { field: "reasoning_content" }, }); expect(translated?.header).toContain("thinking.type = enabled|disabled"); @@ -248,6 +252,19 @@ test("Novita AI sync creates only explicitly verified new reasoners", () => { } }); +test("Novita AI keeps verified DeepSeek and Qwen controls on re-sync", () => { + const context = { authored: () => undefined, existing: () => undefined }; + const pricing = { prompt: { price_per_m_decimal: "0.1" }, completion: { price_per_m_decimal: "0.2" } }; + expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v4-flash-0731", features: ["reasoning"], pricing }), context)?.model) + .toMatchObject({ reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }] }); + expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v4-flash-vision-exp", features: ["reasoning"], pricing }), context)?.model) + .toMatchObject({ reasoning_options: [{ type: "toggle" }] }); + for (const id of ["qwen/qwen3.6-27b", "qwen/qwen3.6-35b-a3b", "qwen/qwen3.6-plus", "qwen/qwen3.8-27b", "qwen/qwen3.8-flash", "qwen/qwen3.8-max"]) { + expect(novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context)?.model) + .toMatchObject({ reasoning_options: [{ type: "toggle" }, { type: "budget_tokens" }] }); + } +}); + test("Novita AI sync inherits capabilities from partial feature lists", () => { const authored: ExistingModel = { name: "DeepSeek", description: "DeepSeek", attachment: false, open_weights: true, diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml index da7bdf06b6f..1a8776fefd6 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-0731.toml @@ -1,6 +1,5 @@ # Toggle: thinking.type = enabled|disabled # Effort: reasoning_effort = low|high|max -# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash-0731" description = "DeepSeek V4 Flash 0731 is a sparse mixture-of-experts model from DeepSeek, with 13B active parameters out of 284B total. This re-post-trained revision is suited for coding, reasoning, and agent workflows." @@ -15,10 +14,6 @@ type = "toggle" type = "effort" values = ["low", "high", "max"] -[[reasoning_options]] -type = "effort" -values = ["low", "high", "max"] - [cost] input = 0.44 output = 1.32 diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml index baa7a475c53..da39f89257f 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml @@ -1,5 +1,4 @@ # Toggle: thinking.type = enabled|disabled -# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash-vision-exp" description = "DeepSeek V4 Flash Vision Exp is an experimental vision-enabled version of DeepSeek V4 Flash 0731(opens in new tab) from DeepSeek, adding image understanding while matching the base model on text capabilities including agents, reasoning, and world knowledge. It is a sparse mixture-of-experts model with 13B active parameters out of 284B total.\n\nIt is suited for document and chart understanding, visual question answering, and multimodal agent workflows that interleave text and images." @@ -10,9 +9,6 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" -[[reasoning_options]] -type = "effort" -values = ["low", "high", "max"] [cost] input = 0.44 diff --git a/providers/novita-ai/models/qwen/qwen3.6-27b.toml b/providers/novita-ai/models/qwen/qwen3.6-27b.toml index 1aaf646daa9..8414fbc003e 100644 --- a/providers/novita-ai/models/qwen/qwen3.6-27b.toml +++ b/providers/novita-ai/models/qwen/qwen3.6-27b.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.6-27b" description = "The Qwen3.6 27B native vision-language dense model builds upon the 3.5-27B version, with key improvements in agentic coding capabilities and enhanced STEM reasoning and inference skills. In the vision modality, it demonstrates significant advances in spatial intelligence, object localization, and detection, while video understanding, document OCR, and visual agent capabilities continue to improve steadily." @@ -9,6 +10,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.6 output = 3.6 diff --git a/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml b/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml index 3109c405547..d660f34a138 100644 --- a/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml +++ b/providers/novita-ai/models/qwen/qwen3.6-35b-a3b.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.6-35b-a3b" name = "Qwen3.6 35B A3B" @@ -10,6 +11,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.248 output = 1.485 diff --git a/providers/novita-ai/models/qwen/qwen3.6-plus.toml b/providers/novita-ai/models/qwen/qwen3.6-plus.toml index 091c9df58b0..c1d7727f8fa 100644 --- a/providers/novita-ai/models/qwen/qwen3.6-plus.toml +++ b/providers/novita-ai/models/qwen/qwen3.6-plus.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.6-plus" name = "Qwen3.6-Plus" @@ -11,6 +12,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.5 output = 3 diff --git a/providers/novita-ai/models/qwen/qwen3.8-27b.toml b/providers/novita-ai/models/qwen/qwen3.8-27b.toml index 386fa956bae..0905ea12fa3 100644 --- a/providers/novita-ai/models/qwen/qwen3.8-27b.toml +++ b/providers/novita-ai/models/qwen/qwen3.8-27b.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.8-27b" @@ -8,6 +9,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.42 output = 3 diff --git a/providers/novita-ai/models/qwen/qwen3.8-flash.toml b/providers/novita-ai/models/qwen/qwen3.8-flash.toml index d62bff12c91..fcc31d471ca 100644 --- a/providers/novita-ai/models/qwen/qwen3.8-flash.toml +++ b/providers/novita-ai/models/qwen/qwen3.8-flash.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.8-flash" description = "Qwen3.8-Flash is Alibaba's multimodal MoE model and an early preview of the Qwen4 architecture: 125B total parameters with only 6B activated per token, plus a 51B N-gram embedding, built on GDN + QSA hybrid attention. It accepts text, image and video input across a 1M-token context and emits up to 131K tokens, with thinking mode on by default and switchable off. Built for coding, agentic workflows, visual\nand long-document understanding, and long-video analysis at a fraction of the cost of comparable frontier models." @@ -9,6 +10,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 0.15 output = 0.47 diff --git a/providers/novita-ai/models/qwen/qwen3.8-max.toml b/providers/novita-ai/models/qwen/qwen3.8-max.toml index 15ada490ebb..0b0150f678d 100644 --- a/providers/novita-ai/models/qwen/qwen3.8-max.toml +++ b/providers/novita-ai/models/qwen/qwen3.8-max.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Budget: thinking_budget (integer reasoning tokens) # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3.8-max" description = " A 2.4T-parameter MoE flagship for coding and knowledge work. Programs autonomously for days to deliver complete projects end to end, with native visual understanding across planning, execution, and verification, plus deep semantic analysis of ultra-long documents and video." @@ -10,6 +11,9 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "budget_tokens" + [cost] input = 2 output = 6 From 8b0d6220d0064af57ecd561bbb0ed6fca3cdc812 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 18 Sep 2026 19:01:12 +0800 Subject: [PATCH 17/17] fix(novita-ai): complete verified reasoning metadata --- packages/core/src/sync/providers/novita-ai.ts | 11 ++++------- packages/core/test/novita-ai.test.ts | 2 +- .../models/deepseek/deepseek-v4-flash-vision-exp.toml | 5 +++++ providers/novita-ai/models/qwen/qwen3-max.toml | 1 + 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/sync/providers/novita-ai.ts b/packages/core/src/sync/providers/novita-ai.ts index fb31483b104..e76b5a043fc 100644 --- a/packages/core/src/sync/providers/novita-ai.ts +++ b/packages/core/src/sync/providers/novita-ai.ts @@ -24,9 +24,7 @@ const VERIFIED_NON_REASONING = new Set([ ]); // Novita accepts the thinking toggle for these routes, but no effort ladder // was verified; do not preserve an inherited guessed ladder from older files. -const VERIFIED_TOGGLE_ONLY = new Set([ - "deepseek/deepseek-v4-flash-vision-exp", -]); +const VERIFIED_TOGGLE_ONLY = new Set(); const VERIFIED_TOGGLE_HEADER = "# Toggle: thinking.type = enabled|disabled\n# Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content.\n"; const VERIFIED_BUDGET_TOGGLE = new Set([ "qwen/qwen3.5-plus", @@ -172,9 +170,9 @@ function buildNovitaModel(model: NovitaAIModel, existing: ExistingModel | undefi : VERIFIED_THINKING_TOGGLE.has(model.id) ? [{ type: "toggle" as const }] : VERIFIED_TOGGLE_ONLY.has(model.id) ? [{ type: "toggle" as const }] : model.id === "deepseek/deepseek-v4-pro" ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["high", "max"] }] - : ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"].includes(model.id) ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["low", "high", "max"] }] + : ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-flash-vision-exp"].includes(model.id) ? [{ type: "toggle" as const }, { type: "effort" as const, values: ["low", "high", "max"] }] : existing?.reasoning_options ?? (model.id === "deepseek/deepseek-r1" ? [] : undefined); - const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) || VERIFIED_BUDGET_TOGGLE.has(model.id) || VERIFIED_TOGGLE_ONLY.has(model.id) || model.id === "deepseek/deepseek-v4-pro" || ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"].includes(model.id) ? { field: "reasoning_content" as const } : undefined); + const interleaved = VERIFIED_NON_REASONING.has(model.id) ? undefined : existing?.interleaved ?? (VERIFIED_THINKING_TOGGLE.has(model.id) || VERIFIED_BUDGET_TOGGLE.has(model.id) || VERIFIED_TOGGLE_ONLY.has(model.id) || model.id === "deepseek/deepseek-v4-pro" || ["deepseek/deepseek-v4.1-flash", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-flash-vision-exp"].includes(model.id) ? { field: "reasoning_content" as const } : undefined); if (existing === undefined && (modelCost === undefined || (reasoning && reasoningOptions === undefined))) return undefined; const values: SyncedFullModel = { name, @@ -236,7 +234,6 @@ export const novitaAi = { // locally curated models solely because a key cannot see them. deleteMissing: false, trackMissingModels: true, - maxMissingFraction: 0.5, missingModelID(model) { // The endpoint also exposes image, embedding, and other non-chat rows. // Only chat models are candidates for a provider catalog TOML/issue. @@ -264,7 +261,7 @@ export const novitaAi = { id: model.id, model: translated, header: ("reasoning_options" in translated && translated.reasoning_options?.some((option) => option.type === "toggle")) - ? `${VERIFIED_TOGGLE_HEADER}${VERIFIED_BUDGET_TOGGLE.has(model.id) ? "# Budget: thinking_budget (integer reasoning tokens)\n" : ""}` + ? `${VERIFIED_TOGGLE_HEADER}${translated.reasoning_options.filter((option) => option.type === "effort").map((option) => `# Effort: reasoning_effort = ${option.values.join("|")}\n`).join("")}${translated.reasoning_options.some((option) => option.type === "budget_tokens") ? "# Budget: thinking_budget (integer reasoning tokens)\n" : ""}` : undefined, }; }, diff --git a/packages/core/test/novita-ai.test.ts b/packages/core/test/novita-ai.test.ts index a5e138d6a54..40ba52d357b 100644 --- a/packages/core/test/novita-ai.test.ts +++ b/packages/core/test/novita-ai.test.ts @@ -258,7 +258,7 @@ test("Novita AI keeps verified DeepSeek and Qwen controls on re-sync", () => { expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v4-flash-0731", features: ["reasoning"], pricing }), context)?.model) .toMatchObject({ reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }] }); expect(novitaAi.translateModel(novitaAiModel({ id: "deepseek/deepseek-v4-flash-vision-exp", features: ["reasoning"], pricing }), context)?.model) - .toMatchObject({ reasoning_options: [{ type: "toggle" }] }); + .toMatchObject({ reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }] }); for (const id of ["qwen/qwen3.6-27b", "qwen/qwen3.6-35b-a3b", "qwen/qwen3.6-plus", "qwen/qwen3.8-27b", "qwen/qwen3.8-flash", "qwen/qwen3.8-max"]) { expect(novitaAi.translateModel(novitaAiModel({ id, features: ["reasoning"], pricing }), context)?.model) .toMatchObject({ reasoning_options: [{ type: "toggle" }, { type: "budget_tokens" }] }); diff --git a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml index da39f89257f..b36a73c7170 100644 --- a/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml +++ b/providers/novita-ai/models/deepseek/deepseek-v4-flash-vision-exp.toml @@ -1,4 +1,5 @@ # Toggle: thinking.type = enabled|disabled +# Effort: reasoning_effort = low|high|max # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "deepseek/deepseek-v4-flash-vision-exp" description = "DeepSeek V4 Flash Vision Exp is an experimental vision-enabled version of DeepSeek V4 Flash 0731(opens in new tab) from DeepSeek, adding image understanding while matching the base model on text capabilities including agents, reasoning, and world knowledge. It is a sparse mixture-of-experts model with 13B active parameters out of 284B total.\n\nIt is suited for document and chart understanding, visual question answering, and multimodal agent workflows that interleave text and images." @@ -9,6 +10,10 @@ field = "reasoning_content" [[reasoning_options]] type = "toggle" +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + [cost] input = 0.44 diff --git a/providers/novita-ai/models/qwen/qwen3-max.toml b/providers/novita-ai/models/qwen/qwen3-max.toml index 28548d19f25..e224ca3bff1 100644 --- a/providers/novita-ai/models/qwen/qwen3-max.toml +++ b/providers/novita-ai/models/qwen/qwen3-max.toml @@ -1,5 +1,6 @@ # Toggle: thinking.type = enabled|disabled # Budget: thinking_budget (integer reasoning tokens) +# Verified with Novita chat/completions on 2026-09-18: thinking.type=enabled returns reasoning_content; thinking_budget=64 returns 64 reasoning tokens. # Verified with Novita chat/completions on 2026-09-17: disabling removes reasoning_content. base_model = "alibaba/qwen3-max" description = "Flagship Qwen model for complex reasoning, coding, and agentic workflows"