From 54f3e6d5b9ffbdda501c4c9dd75ef00632633fee Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 06:13:57 +0000 Subject: [PATCH 01/29] feat: add Neosantara provider Neosantara is an OpenAI-compatible LLM gateway. Both catalog endpoints used by the sync are public and need no API key: - https://api.neosantara.xyz/v1/models - https://api.neosantara.xyz/v1/public/pricing The sync module registers with the existing provider-sync workflow, so the catalog is refreshed by the usual periodic automation. Scope of the generated catalog: - text models with at least 100k context that advertise function calling - image generation models that have a canonical models/ entry - deprecated models are excluded Pricing is published in USD per million tokens, applying the customer-facing discount and converting IDR list prices with the exchange rate the pricing endpoint reports. Per-image pricing is left unpublished because models.dev has no field for it. Reasoning options are reviewed per model: each effort list is the lab/peer set intersected with what the public request schema accepts, and models that expose no caller control author an empty set. Provider files stay override-only via base_model, so limits, modalities and benchmarks are inherited from the canonical lab entries. --- packages/core/src/sync/index.ts | 4 + .../core/src/sync/providers/neosantara.ts | 359 +++++++++++++ packages/core/test/neosantara-sync.test.ts | 475 ++++++++++++++++++ .../neosantara/models/claude-4.5-opus.toml | 12 + .../neosantara/models/claude-4.5-sonnet.toml | 12 + .../neosantara/models/claude-fable-5.toml | 24 + .../neosantara/models/claude-opus-4-6.toml | 16 + .../neosantara/models/claude-opus-5.toml | 13 + .../neosantara/models/claude-opus-7.toml | 23 + .../neosantara/models/claude-opus-8.toml | 23 + .../neosantara/models/claude-sonnet-4-6.toml | 16 + .../neosantara/models/claude-sonnet-5.toml | 23 + .../models/deepseek-v4-flash-0731.toml | 7 + .../neosantara/models/deepseek-v4-flash.toml | 12 + .../models/deepseek-v4-pro-0813.toml | 10 + .../neosantara/models/deepseek-v4-pro.toml | 12 + providers/neosantara/models/devstral-2.toml | 9 + .../models/gemini-3.1-flash-lite.toml | 13 + .../neosantara/models/gemini-3.5-flash.toml | 14 + .../neosantara/models/gemini-3.6-flash.toml | 14 + .../neosantara/models/gemini-3.7-flash.toml | 14 + .../neosantara/models/glm-4.5-flash.toml | 14 + .../neosantara/models/glm-4.6v-flash.toml | 10 + .../neosantara/models/glm-4.7-flash.toml | 10 + providers/neosantara/models/glm-4.7.toml | 14 + .../neosantara/models/glm-5.3-flash.toml | 14 + providers/neosantara/models/gpt-5-nano.toml | 10 + providers/neosantara/models/gpt-5.4-mini.toml | 16 + providers/neosantara/models/gpt-5.4-nano.toml | 6 + providers/neosantara/models/gpt-5.4.toml | 15 + providers/neosantara/models/gpt-5.5.toml | 15 + providers/neosantara/models/gpt-5.6-luna.toml | 18 + providers/neosantara/models/gpt-5.6-sol.toml | 18 + .../neosantara/models/gpt-5.6-terra.toml | 17 + providers/neosantara/models/gpt-image-2.toml | 1 + providers/neosantara/models/gpt-oss-120b.toml | 7 + providers/neosantara/models/gpt-oss-20b.toml | 6 + .../neosantara/models/kimi-k2-thinking.toml | 10 + providers/neosantara/models/kimi-k2.5.toml | 13 + providers/neosantara/models/kimi-k2.6.toml | 10 + providers/neosantara/models/kimi-k3.toml | 11 + providers/neosantara/models/laguna-s-2.1.toml | 12 + .../neosantara/models/laguna-xs-2.1.toml | 9 + .../neosantara/models/ling-3.0-flash-fin.toml | 10 + .../llama-3.3-nemotron-super-49b-v1.5.toml | 10 + providers/neosantara/models/minimax-m2.7.toml | 7 + providers/neosantara/models/minimax-m3.toml | 14 + .../models/mistral-large-latest.toml | 13 + .../models/mistral-small-latest.toml | 14 + .../neosantara/models/muse-glimmer-30b.toml | 7 + .../neosantara/models/muse-spark-1.1.toml | 11 + .../neosantara/models/step-3.5-flash.toml | 7 + providers/neosantara/provider.toml | 5 + 53 files changed, 1459 insertions(+) create mode 100644 packages/core/src/sync/providers/neosantara.ts create mode 100644 packages/core/test/neosantara-sync.test.ts create mode 100644 providers/neosantara/models/claude-4.5-opus.toml create mode 100644 providers/neosantara/models/claude-4.5-sonnet.toml create mode 100644 providers/neosantara/models/claude-fable-5.toml create mode 100644 providers/neosantara/models/claude-opus-4-6.toml create mode 100644 providers/neosantara/models/claude-opus-5.toml create mode 100644 providers/neosantara/models/claude-opus-7.toml create mode 100644 providers/neosantara/models/claude-opus-8.toml create mode 100644 providers/neosantara/models/claude-sonnet-4-6.toml create mode 100644 providers/neosantara/models/claude-sonnet-5.toml create mode 100644 providers/neosantara/models/deepseek-v4-flash-0731.toml create mode 100644 providers/neosantara/models/deepseek-v4-flash.toml create mode 100644 providers/neosantara/models/deepseek-v4-pro-0813.toml create mode 100644 providers/neosantara/models/deepseek-v4-pro.toml create mode 100644 providers/neosantara/models/devstral-2.toml create mode 100644 providers/neosantara/models/gemini-3.1-flash-lite.toml create mode 100644 providers/neosantara/models/gemini-3.5-flash.toml create mode 100644 providers/neosantara/models/gemini-3.6-flash.toml create mode 100644 providers/neosantara/models/gemini-3.7-flash.toml create mode 100644 providers/neosantara/models/glm-4.5-flash.toml create mode 100644 providers/neosantara/models/glm-4.6v-flash.toml create mode 100644 providers/neosantara/models/glm-4.7-flash.toml create mode 100644 providers/neosantara/models/glm-4.7.toml create mode 100644 providers/neosantara/models/glm-5.3-flash.toml create mode 100644 providers/neosantara/models/gpt-5-nano.toml create mode 100644 providers/neosantara/models/gpt-5.4-mini.toml create mode 100644 providers/neosantara/models/gpt-5.4-nano.toml create mode 100644 providers/neosantara/models/gpt-5.4.toml create mode 100644 providers/neosantara/models/gpt-5.5.toml create mode 100644 providers/neosantara/models/gpt-5.6-luna.toml create mode 100644 providers/neosantara/models/gpt-5.6-sol.toml create mode 100644 providers/neosantara/models/gpt-5.6-terra.toml create mode 100644 providers/neosantara/models/gpt-image-2.toml create mode 100644 providers/neosantara/models/gpt-oss-120b.toml create mode 100644 providers/neosantara/models/gpt-oss-20b.toml create mode 100644 providers/neosantara/models/kimi-k2-thinking.toml create mode 100644 providers/neosantara/models/kimi-k2.5.toml create mode 100644 providers/neosantara/models/kimi-k2.6.toml create mode 100644 providers/neosantara/models/kimi-k3.toml create mode 100644 providers/neosantara/models/laguna-s-2.1.toml create mode 100644 providers/neosantara/models/laguna-xs-2.1.toml create mode 100644 providers/neosantara/models/ling-3.0-flash-fin.toml create mode 100644 providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml create mode 100644 providers/neosantara/models/minimax-m2.7.toml create mode 100644 providers/neosantara/models/minimax-m3.toml create mode 100644 providers/neosantara/models/mistral-large-latest.toml create mode 100644 providers/neosantara/models/mistral-small-latest.toml create mode 100644 providers/neosantara/models/muse-glimmer-30b.toml create mode 100644 providers/neosantara/models/muse-spark-1.1.toml create mode 100644 providers/neosantara/models/step-3.5-flash.toml create mode 100644 providers/neosantara/provider.toml diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index f08532afa14..ec9e3f5d864 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -26,6 +26,7 @@ import { kilo } from "./providers/kilo.js"; import { llmgateway, llmgatewayProviders } from "./providers/llmgateway.js"; import { mergeGateway } from "./providers/merge-gateway.js"; import { nanoGpt } from "./providers/nano-gpt.js"; +import { neosantara } from "./providers/neosantara.js"; import { openai } from "./providers/openai.js"; import { ofox } from "./providers/ofox.js"; import { openrouter } from "./providers/openrouter.js"; @@ -150,6 +151,7 @@ export const providers: { "llmgateway-providers": SyncProvider; "merge-gateway": SyncProvider; "nano-gpt": SyncProvider; + neosantara: SyncProvider; ofox: SyncProvider; openai: SyncProvider; openrouter: SyncProvider; @@ -184,6 +186,7 @@ export const providers: { "llmgateway-providers": llmgatewayProviders, "merge-gateway": mergeGateway, "nano-gpt": nanoGpt, + neosantara, ofox, openai, openrouter, @@ -209,6 +212,7 @@ export const groups = { "llmgateway-providers", "merge-gateway", "nano-gpt", + "neosantara", "ofox", "requesty", "openrouter", diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts new file mode 100644 index 00000000000..f4f188b4998 --- /dev/null +++ b/packages/core/src/sync/providers/neosantara.ts @@ -0,0 +1,359 @@ +import { z } from "zod"; +import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; +import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; + +const MODELS_ENDPOINT = "https://api.neosantara.xyz/v1/models"; +const PRICING_ENDPOINT = "https://api.neosantara.xyz/v1/public/pricing"; +const MIN_CONTEXT_WINDOW = 100_000; + +const Pricing = z.object({ + currency: z.enum(["USD", "IDR"]), + prompt: z.number().nonnegative().optional(), + completion: z.number().nonnegative().optional(), + cache_read: z.number().nonnegative().optional(), + cache_write: z.number().nonnegative().optional(), +}).passthrough(); + +export const NeosantaraModel = z.object({ + id: z.string(), + object: z.literal("model"), + created: z.number(), + owned_by: z.string(), + description: z.string(), + context_window: z.number().int().nonnegative().nullable(), + max_output_tokens: z.number().int().nonnegative().nullable(), + pricing: Pricing, + discount: z.number().min(0).max(100).optional(), + capabilities: z.array(z.string()), + deprecated: z.boolean(), + deprecation_alternatives: z.array(z.string()), +}).passthrough(); + +export const NeosantaraModelsResponse = z.object({ + object: z.literal("list"), + data: z.array(NeosantaraModel), +}).passthrough(); + +const PublicPricing = z.object({ + currency: z.enum(["USD", "IDR"]).optional(), + per_million_tokens: z.object({ + input: z.number().nonnegative().optional(), + output: z.number().nonnegative().optional(), + cache_read: z.number().nonnegative().optional(), + cache_write: z.number().nonnegative().optional(), + }).optional(), +}).passthrough(); + +// Only `name` and `raw_pricing` are consumed. Everything else is descriptive and optional so a +// single omission cannot fail the whole catalog sync. +const PublicPricingModel = z.object({ + name: z.string(), + capabilities: z.array(z.string()).optional(), + context_window: z.number().int().nonnegative().nullable().optional(), + description: z.string().optional(), + raw_pricing: PublicPricing, +}).passthrough(); + +export const NeosantaraPricingResponse = z.object({ + data: z.array(PublicPricingModel), + meta: z.object({ + count: z.number().int().nonnegative(), + updated_at: z.string(), + exchange_rate: z.object({ + usd_idr: z.number().positive(), + currency: z.literal("IDR"), + source: z.string(), + }), + }), +}).passthrough(); + +const NeosantaraCombinedResponse = z.object({ + models: z.unknown(), + pricing: z.unknown(), +}); + +export type NeosantaraSourceModel = z.infer & { + exchange_rate: number; +}; + +/** + * Neosantara serves bare model ids, so the canonical metadata tree resolves most of them on + * its own and newly launched models are picked up without a code change. Only ids the tree + * cannot resolve are aliased here (renamed generations, aliases pointing at another model, + * and vendor-specific suffixes). + */ +const BASE_MODEL_ALIASES: Record = { + "claude-3-haiku": "anthropic/claude-3-haiku-20240307", + "claude-4.5-opus": "anthropic/claude-opus-4-5", + "claude-4.5-sonnet": "anthropic/claude-sonnet-4-5", + "claude-opus-7": "anthropic/claude-opus-4-7", + "claude-opus-8": "anthropic/claude-opus-4-8", + "devstral-2": "mistral/devstral-2512", + // Neosantara routes its coding alias at Grok 4.3. + "grok-code-fast": "xai/grok-4.3", + "qwen3-235b-wse": "alibaba/qwen3-235b-a22b-instruct-2507", +}; + +export function resolveNeosantaraBaseModel(id: string) { + return BASE_MODEL_ALIASES[id] ?? resolveModelMetadataBaseModel(id); +} + +export function mergeNeosantaraCatalogs( + models: z.infer, + pricing: z.infer, +): NeosantaraSourceModel[] { + const modelPricing = new Map(models.data.map((model) => [model.id, model.pricing] as const)); + const publicPricing = new Map(pricing.data.map((model) => { + const raw = model.raw_pricing; + const tokens = raw.per_million_tokens; + // Cache rates may only be surfaced by /v1/models. Inherit them so publishing does not + // silently drop cost data, but never across a currency change: the two figures would + // otherwise be mixed into one scale. + const fallback = modelPricing.get(model.name); + const cache = fallback?.currency === raw.currency ? fallback : undefined; + const normalized = + raw.currency === undefined || tokens?.input === undefined || tokens.output === undefined + ? undefined + : { + currency: raw.currency, + prompt: tokens.input, + completion: tokens.output, + cache_read: tokens.cache_read ?? cache?.cache_read, + cache_write: tokens.cache_write ?? cache?.cache_write, + }; + return [model.name, normalized] as const; + })); + return models.data.map((model) => ({ + ...model, + pricing: publicPricing.get(model.id) ?? model.pricing, + exchange_rate: pricing.meta.exchange_rate.usd_idr, + })); +} + +type ReasoningControls = NonNullable; + +/** + * Reviewed reasoning controls per model (AGENTS.md → "Reasoning options"). + * + * Neosantara is a multi-model relay, so each effort list is the underlying lab/peer set + * intersected with what this host actually accepts and passes on. The public request schema + * takes `reasoning_effort` of none|minimal|low|medium|high|xhigh only, so peer values such as + * `max` are dropped, and `minimal` is never added unless the lab/peers list it. `[]` means the + * model reasons but exposes no caller control on this host. + * + * Every entry was verified against this host's public request surface. Internal routing is + * deliberately not described here. + */ +const EFFORT_GPT_5_6: ReasoningControls = [ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, +]; +const EFFORT_LOW_HIGH: ReasoningControls = [{ type: "effort", values: ["low", "high"] }]; +const EFFORT_LOW_MEDIUM_HIGH: ReasoningControls = [ + { type: "effort", values: ["low", "medium", "high"] }, +]; +const THINKING_BUDGET: ReasoningControls = [ + { type: "toggle" }, + { type: "budget_tokens", min: 1_024 }, +]; +const THINKING_BUDGET_HEADER = + "# Toggle: /v1/messages thinking.type = enabled|disabled\n# Budget: /v1/messages thinking.budget_tokens (reasoning tokens)\n"; +const NO_CALLER_CONTROL: ReasoningControls = []; + +const REASONING_CONTROLS: Record = { + // `reasoning_effort` is accepted and honoured for these models. + "gpt-5.6-sol": { options: EFFORT_GPT_5_6 }, + "gpt-5.6-terra": { options: EFFORT_GPT_5_6 }, + "gpt-5.6-luna": { options: EFFORT_GPT_5_6 }, + "claude-opus-5": { options: [{ type: "effort", values: ["low", "medium", "high", "xhigh"] }] }, + // Graded effort is honoured, but only the levels the lab and its peers document. + "deepseek-v4-pro-0813": { options: EFFORT_LOW_HIGH }, + "glm-5.3-flash": { options: EFFORT_LOW_HIGH }, + // Peers are split: several author a toggle this host cannot express, so the effort list + // follows the same-surface effort peers (pioneer, opencode). + "laguna-s-2.1": { options: EFFORT_LOW_MEDIUM_HIGH }, + "laguna-xs-2.1": { options: EFFORT_LOW_MEDIUM_HIGH }, + // Thinking is a plain on/off here; no graded effort reaches the model. + "glm-4.6v-flash": { + options: [{ type: "toggle" }], + header: "# Toggle: thinking.type = enabled (off = field omitted)\n", + }, + // Graded effort is not honoured for these; the caller steers them with a thinking budget. + "claude-fable-5": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, + "claude-sonnet-5": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, + "claude-opus-7": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, + "claude-opus-8": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, + // Models that reason but accept no reasoning control on this host. + "claude-sonnet-4-6": { options: NO_CALLER_CONTROL }, + "claude-opus-4-6": { options: NO_CALLER_CONTROL }, + "deepseek-v4-flash": { options: NO_CALLER_CONTROL }, + "deepseek-v4-pro": { options: NO_CALLER_CONTROL }, + "gemini-3.6-flash": { options: NO_CALLER_CONTROL }, + "gemini-3.7-flash": { options: NO_CALLER_CONTROL }, + "gpt-5-nano": { options: NO_CALLER_CONTROL }, + "gpt-5.4": { options: NO_CALLER_CONTROL }, + "gpt-5.4-nano": { options: NO_CALLER_CONTROL }, + "gpt-5.5": { options: NO_CALLER_CONTROL }, + "kimi-k2-thinking": { options: NO_CALLER_CONTROL }, + "kimi-k2.5": { options: NO_CALLER_CONTROL }, + "kimi-k2.6": { options: NO_CALLER_CONTROL }, + "kimi-k3": { options: NO_CALLER_CONTROL }, + "ling-3.0-flash-fin": { options: NO_CALLER_CONTROL }, + "minimax-m2.7": { options: NO_CALLER_CONTROL }, + "muse-spark-1.1": { options: NO_CALLER_CONTROL }, + "step-3.5-flash": { options: NO_CALLER_CONTROL }, +}; + +/** Image models are priced per image and carry no context window, so they skip the token filters. */ +function isImageModel(model: NeosantaraSourceModel) { + return model.capabilities.includes("image_generation"); +} + +/** Display names for ids whose canonical entry is a different model (see BASE_MODEL_ALIASES). */ +const NAME_OVERRIDES: Record = { + "grok-code-fast": "Grok Code Fast", +}; + +/** Models this host publishes at all, regardless of whether we can translate them yet. */ +export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { + return ( + isImageModel(model) || + (model.context_window !== null && + model.context_window >= MIN_CONTEXT_WINDOW && + model.capabilities.includes("function_calling")) + ); +} + +export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { + if (model.deprecated || resolveNeosantaraBaseModel(model.id) === undefined) return false; + if (!meetsNeosantaraPublicFilter(model)) return false; + if (isImageModel(model)) return true; + // Skip rather than throw: one missing catalog field must cost a single model, not the run. + if (model.pricing.prompt === undefined || model.pricing.completion === undefined) return false; + return !model.capabilities.includes("reasoning") || REASONING_CONTROLS[model.id] !== undefined; +} + +function effectiveUsd(value: number | undefined, model: NeosantaraSourceModel) { + if (value === undefined) return undefined; + const usd = model.pricing.currency === "IDR" ? value / model.exchange_rate : value; + const discount = model.discount === undefined ? 1 : (100 - model.discount) / 100; + return Math.round(usd * discount * 1_000_000) / 1_000_000; +} + +/** Input modalities this host accepts, derived from the advertised understanding capabilities. */ +export function neosantaraInputModalities(capabilities: string[]) { + return [ + "text" as const, + ...(capabilities.includes("vision") ? (["image"] as const) : []), + ...(capabilities.includes("video_understanding") ? (["video"] as const) : []), + ]; +} + +export function buildNeosantaraModel( + model: NeosantaraSourceModel, + existing: ExistingModel | undefined, +): SyncedModel { + const baseModel = resolveNeosantaraBaseModel(model.id); + if (baseModel === undefined) { + throw new Error(`No canonical base model mapping for Neosantara model '${model.id}'`); + } + + const limit = { + context: model.context_window ?? undefined, + }; + + // models.dev has no per-image cost field, so per-image pricing is left unpublished and + // every other trait is inherited from the canonical lab entry. + if (isImageModel(model)) { + return factorBaseModel( + baseModel, + { status: model.deprecated ? "deprecated" : existing?.status }, + limit, + existing?.base_model === baseModel ? existing.base_model_omit : undefined, + ); + } + + const reasoning = model.capabilities.includes("reasoning"); + const inputCost = effectiveUsd(model.pricing.prompt, model); + const outputCost = effectiveUsd(model.pricing.completion, model); + if (inputCost === undefined || outputCost === undefined) { + throw new Error(`Missing token pricing for Neosantara model '${model.id}'`); + } + const cost = { + input: inputCost, + output: outputCost, + cache_read: effectiveUsd(model.pricing.cache_read, model), + cache_write: effectiveUsd(model.pricing.cache_write, model), + }; + + return factorBaseModel( + baseModel, + { + name: NAME_OVERRIDES[model.id], + reasoning, + reasoning_options: reasoning ? REASONING_CONTROLS[model.id]?.options ?? [] : undefined, + attachment: model.capabilities.includes("vision"), + tool_call: true, + structured_output: model.capabilities.includes("json_mode"), + modalities: { + input: neosantaraInputModalities(model.capabilities), + output: ["text"], + }, + status: model.deprecated ? "deprecated" : existing?.status, + limit, + cost, + }, + limit, + existing?.base_model === baseModel ? existing.base_model_omit : undefined, + ); +} + +async function fetchJson(url: string) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Neosantara request failed: ${response.status} ${response.statusText}`); + } + return response.json(); +} + +export const neosantara = { + id: "neosantara", + name: "Neosantara", + modelsDir: "providers/neosantara/models", + authoritativeHeaders: true, + sourceID(model) { + if (model.deprecated) return undefined; + // Report anything the public filter accepts but translateModel skipped, whether the cause is + // a missing canonical entry, unusable upstream pricing, or controls that still need review. + return meetsNeosantaraPublicFilter(model) ? model.id : undefined; + }, + skippedNotice(ids) { + if (ids.length === 0) return []; + return [ + `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit, because the Neosantara catalog reported no usable token pricing, or because their reasoning controls still need review against this host's request schema.`, + `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, + "Add a `models//.toml` entry (and, for reasoning models, a reviewed control set) to include them in the next sync.", + ]; + }, + async fetchModels() { + const [models, pricing] = await Promise.all([ + fetchJson(MODELS_ENDPOINT), + fetchJson(PRICING_ENDPOINT), + ]); + return { models, pricing }; + }, + parseModels(raw) { + const combined = NeosantaraCombinedResponse.parse(raw); + return mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(combined.models), + NeosantaraPricingResponse.parse(combined.pricing), + ); + }, + translateModel(model, context) { + if (!shouldSyncNeosantaraModel(model)) return undefined; + return { + id: model.id, + model: buildNeosantaraModel(model, context.existing(model.id)), + header: REASONING_CONTROLS[model.id]?.header, + }; + }, +} satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts new file mode 100644 index 00000000000..f77b9157105 --- /dev/null +++ b/packages/core/test/neosantara-sync.test.ts @@ -0,0 +1,475 @@ +import { expect, test } from "bun:test"; + +import { + buildNeosantaraModel, + mergeNeosantaraCatalogs, + neosantara, + neosantaraInputModalities, + NeosantaraModelsResponse, + NeosantaraPricingResponse, + resolveNeosantaraBaseModel, + shouldSyncNeosantaraModel, +} from "../src/sync/providers/neosantara.js"; + +const modelsResponse = { + object: "list", + data: [ + { + id: "gemini-3.7-flash", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "Fast reasoning model", + context_window: 1_000_000, + max_output_tokens: 8_192, + pricing: { + currency: "USD", + prompt: 1.5, + completion: 7.5, + cache_read: 0.15, + cache_write: 1.5, + }, + discount: 50, + capabilities: ["text_generation", "function_calling", "json_mode", "vision", "reasoning"], + deprecated: false, + deprecation_alternatives: [], + }, + { + id: "gpt-oss-20b", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "IDR-priced open model", + context_window: 131_072, + max_output_tokens: 8_192, + pricing: { currency: "IDR", prompt: 400, completion: 1_600 }, + capabilities: ["text_generation", "function_calling"], + deprecated: false, + deprecation_alternatives: [], + }, + { + id: "too-small", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "Too small", + context_window: 32_000, + max_output_tokens: 4_096, + pricing: { currency: "USD", prompt: 1, completion: 2 }, + capabilities: ["function_calling"], + deprecated: false, + deprecation_alternatives: [], + }, + { + id: "no-tools", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "No tools", + context_window: 128_000, + max_output_tokens: 4_096, + pricing: { currency: "USD", prompt: 1, completion: 2 }, + capabilities: ["text_generation"], + deprecated: false, + deprecation_alternatives: [], + }, + ], +}; + +const pricingResponse = { + data: modelsResponse.data.map((model) => ({ + name: model.id, + provider: "Neosantara", + category: "standard", + type: "text", + input: "-", + output: "-", + capabilities: model.capabilities, + context_window: model.context_window, + description: model.description, + raw_pricing: { + currency: model.pricing.currency, + per_million_tokens: { + input: model.pricing.prompt, + output: model.pricing.completion, + cache_read: "cache_read" in model.pricing ? model.pricing.cache_read : undefined, + cache_write: "cache_write" in model.pricing ? model.pricing.cache_write : undefined, + }, + }, + })), + meta: { + count: modelsResponse.data.length, + updated_at: "2026-09-02T16:32:29.553Z", + exchange_rate: { usd_idr: 20_000, currency: "IDR", source: "test" }, + }, +}; + +test("parses and merges both public Neosantara endpoints", () => { + const models = NeosantaraModelsResponse.parse(modelsResponse); + const pricing = NeosantaraPricingResponse.parse(pricingResponse); + const merged = mergeNeosantaraCatalogs(models, pricing); + + expect(merged).toHaveLength(4); + expect(merged[0]?.exchange_rate).toBe(20_000); + expect(merged[0]?.pricing.prompt).toBe(1.5); +}); + +test("filters to 100k+ context, function calling, and known canonical base models", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + ); + + expect(shouldSyncNeosantaraModel(merged[0]!)).toBe(true); + expect(shouldSyncNeosantaraModel(merged[2]!)).toBe(false); + expect(shouldSyncNeosantaraModel(merged[3]!)).toBe(false); + expect(resolveNeosantaraBaseModel("gemini-3.7-flash")).toBe("google/gemini-3.7-flash"); + expect(resolveNeosantaraBaseModel("unknown-model")).toBeUndefined(); +}); + +test("builds override-only models with effective USD pricing and host reasoning controls", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + ); + + const gemini = buildNeosantaraModel(merged[0]!, undefined); + expect(gemini).toMatchObject({ + base_model: "google/gemini-3.7-flash", + reasoning_options: [], + limit: { context: 1_000_000 }, + cost: { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 }, + }); + + const idr = buildNeosantaraModel(merged[1]!, undefined); + expect(idr).toMatchObject({ + base_model: "openai/gpt-oss-20b", + reasoning: false, + cost: { input: 0.02, output: 0.08 }, + }); +}); + +test("authors reviewed per-host reasoning controls instead of dumping the request enum", () => { + const source = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!; + const build = (id: string) => buildNeosantaraModel({ ...source, id }, undefined); + + expect(build("gpt-5.6-terra").reasoning_options).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); + expect(build("claude-opus-5").reasoning_options).toEqual([ + { type: "effort", values: ["low", "medium", "high", "xhigh"] }, + ]); + expect(build("claude-fable-5").reasoning_options).toEqual([ + { type: "toggle" }, + { type: "budget_tokens", min: 1_024 }, + ]); + expect(build("glm-4.6v-flash").reasoning_options).toEqual([{ type: "toggle" }]); + expect(build("kimi-k2-thinking").reasoning_options).toEqual([]); + expect(build("gpt-5.4").reasoning_options).toEqual([]); + + for (const id of ["gpt-5.6-terra", "claude-opus-5", "glm-5.3-flash", "laguna-s-2.1"]) { + const values = build(id).reasoning_options?.flatMap((option) => + "values" in option ? option.values : [], + ); + expect(values).not.toContain("minimal"); + expect(values).not.toContain("max"); + } +}); + +test("refuses to sync a reasoning model that has no reviewed control set", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + ); + + const unreviewed = { + ...merged[1]!, + capabilities: [...merged[1]!.capabilities, "reasoning"], + }; + + expect(shouldSyncNeosantaraModel(merged[1]!)).toBe(true); + expect(shouldSyncNeosantaraModel(unreviewed)).toBe(false); +}); + +test("keeps model pricing when the public pricing entry is only partially populated", () => { + const partial = structuredClone(pricingResponse) as typeof pricingResponse; + const entry = partial.data.find((row) => row.name === "gemini-3.7-flash")!; + delete (entry.raw_pricing.per_million_tokens as { output?: number }).output; + + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(partial), + ); + + const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; + expect(gemini.pricing.prompt).toBe(1.5); + expect(gemini.pricing.completion).toBe(7.5); + + expect(buildNeosantaraModel(gemini, undefined).cost).toMatchObject({ + input: 0.75, + output: 3.75, + }); +}); + +test("derives input modalities from every advertised understanding capability", () => { + expect(neosantaraInputModalities(["text_generation"])).toEqual(["text"]); + expect(neosantaraInputModalities(["text_generation", "vision"])).toEqual(["text", "image"]); + expect(neosantaraInputModalities(["vision", "video_understanding"])).toEqual([ + "text", + "image", + "video", + ]); + + const source = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!; + + // zhipuai/glm-4.6v-flash accepts video upstream, so the host must not narrow the lab entry. + const video = buildNeosantaraModel( + { + ...source, + id: "glm-4.6v-flash", + capabilities: [...source.capabilities, "video_understanding"], + }, + undefined, + ); + expect(video.modalities).toBeUndefined(); +}); + +const imageModelsResponse = { + object: "list", + data: [ + { + id: "gpt-image-2", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "Image generation and editing", + context_window: 0, + max_output_tokens: 0, + pricing: { currency: "USD", per_image: { "1024x1024": 0.04 } }, + capabilities: ["image_generation"], + deprecated: false, + deprecation_alternatives: [], + }, + { + id: "imagen-4.0-fast", + object: "model", + created: 1_700_000_000, + owned_by: "Neosantara", + description: "Image generation without a canonical lab entry", + context_window: 0, + max_output_tokens: 0, + pricing: { currency: "USD", per_image: { "1024x1024": 0.02 } }, + capabilities: ["image_generation"], + deprecated: false, + deprecation_alternatives: [], + }, + ], +}; + +const imagePricingResponse = { + data: imageModelsResponse.data.map((model) => ({ + name: model.id, + provider: "Neosantara", + category: "standard", + type: "image", + input: "-", + output: "-", + capabilities: model.capabilities, + context_window: model.context_window, + description: model.description, + raw_pricing: { currency: "USD", per_image: model.pricing.per_image }, + })), + meta: { + count: imageModelsResponse.data.length, + updated_at: "2026-09-02T16:32:29.553Z", + exchange_rate: { usd_idr: 16_000, currency: "IDR", source: "test" }, + }, +}; + +test("syncs image generation models on per-image pricing without token costs", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(imageModelsResponse), + NeosantaraPricingResponse.parse(imagePricingResponse), + ); + + const image = merged[0]!; + expect(shouldSyncNeosantaraModel(image)).toBe(true); + + const built = buildNeosantaraModel(image, undefined); + expect("base_model" in built && built.base_model).toBe("openai/gpt-image-2"); + expect(built.cost).toBeUndefined(); + expect(built.reasoning_options).toBeUndefined(); + expect(built.tool_call).toBeUndefined(); +}); + +test("reports image models that still lack a canonical lab entry", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(imageModelsResponse), + NeosantaraPricingResponse.parse(imagePricingResponse), + ); + + const context = { existing: () => undefined, authored: () => undefined }; + + // The mapped model syncs, so it is never handed to the skip reporter. + expect(neosantara.translateModel(merged[0]!, context)?.id).toBe("gpt-image-2"); + + expect(shouldSyncNeosantaraModel(merged[1]!)).toBe(false); + expect(neosantara.translateModel(merged[1]!, context)).toBeUndefined(); + expect(neosantara.sourceID(merged[1]!)).toBe("imagen-4.0-fast"); +}); + +test("preserves model cache pricing when the public entry omits it in the same currency", () => { + const partial = structuredClone(pricingResponse) as typeof pricingResponse; + const entry = partial.data.find((row) => row.name === "gemini-3.7-flash")!; + const tokens = entry.raw_pricing.per_million_tokens as { + cache_read?: number; + cache_write?: number; + }; + delete tokens.cache_read; + delete tokens.cache_write; + + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(partial), + ); + + const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; + expect(gemini.pricing.cache_read).toBe(0.15); + expect(gemini.pricing.cache_write).toBe(1.5); +}); + +test("never carries cache pricing across a currency change between the two endpoints", () => { + const crossCurrency = structuredClone(pricingResponse) as typeof pricingResponse; + const entry = crossCurrency.data.find((row) => row.name === "gemini-3.7-flash")!; + entry.raw_pricing.currency = "IDR"; + const tokens = entry.raw_pricing.per_million_tokens as { + input?: number; + output?: number; + cache_read?: number; + cache_write?: number; + }; + tokens.input = 24_000; + tokens.output = 120_000; + delete tokens.cache_read; + delete tokens.cache_write; + + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(crossCurrency), + ); + + const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; + expect(gemini.pricing.currency).toBe("IDR"); + expect(gemini.pricing.cache_read).toBeUndefined(); + expect(gemini.pricing.cache_write).toBeUndefined(); +}); + +test("tolerates public pricing entries that omit fields the sync never reads", () => { + const sparse = { + data: [ + { name: "gemini-3.7-flash", raw_pricing: { per_image: 0, currency: "USD" } }, + ...pricingResponse.data.filter((row) => row.name !== "gemini-3.7-flash"), + ], + meta: pricingResponse.meta, + }; + + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(sparse), + ); + + const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; + expect(gemini.pricing.prompt).toBe(1.5); + expect(gemini.pricing.completion).toBe(7.5); +}); + +test("resolves base models from the canonical metadata tree, aliasing only ambiguous ids", () => { + // No alias entry needed: the canonical filename is unique under models/. + expect(resolveNeosantaraBaseModel("gpt-5.6-terra")).toBe("openai/gpt-5.6-terra"); + expect(resolveNeosantaraBaseModel("kimi-k2.5")).toBe("moonshotai/kimi-k2.5"); + expect(resolveNeosantaraBaseModel("gpt-image-2")).toBe("openai/gpt-image-2"); + + // Aliases cover ids the tree cannot resolve on its own. + expect(resolveNeosantaraBaseModel("grok-code-fast")).toBe("xai/grok-4.3"); + expect(resolveNeosantaraBaseModel("claude-opus-7")).toBe("anthropic/claude-opus-4-7"); + expect(resolveNeosantaraBaseModel("qwen3-235b-wse")).toBe( + "alibaba/qwen3-235b-a22b-instruct-2507", + ); + + expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); +}); + +test("reports reasoning models held back for control review instead of dropping them silently", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + ); + + const unreviewed = { + ...merged[1]!, + capabilities: [...merged[1]!.capabilities, "reasoning"], + }; + + expect(shouldSyncNeosantaraModel(unreviewed)).toBe(false); + expect(neosantara.sourceID(unreviewed)).toBe("gpt-oss-20b"); +}); + +test("surfaces skipped models in the sync notice so they are not collected and discarded", () => { + expect(neosantara.skippedNotice([])).toEqual([]); + + const notice = neosantara.skippedNotice(["gemini-3.1-pro", "imagen-4.0-fast"]); + expect(notice.length).toBeGreaterThan(0); + expect(notice.join("\n")).toContain("gemini-3.1-pro"); + expect(notice.join("\n")).toContain("imagen-4.0-fast"); +}); + +test("skips a model with unusable upstream pricing instead of aborting the whole sync", () => { + const broken = { + object: "list", + data: [ + { + ...modelsResponse.data[0]!, + id: "kimi-k2.5", + pricing: { currency: "USD" }, + discount: undefined, + capabilities: ["text_generation", "function_calling"], + }, + ], + }; + + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(broken), + NeosantaraPricingResponse.parse({ data: [], meta: pricingResponse.meta }), + ); + + const context = { existing: () => undefined, authored: () => undefined }; + expect(() => neosantara.translateModel(merged[0]!, context)).not.toThrow(); + expect(neosantara.translateModel(merged[0]!, context)).toBeUndefined(); + expect(neosantara.sourceID(merged[0]!)).toBe("kimi-k2.5"); +}); + +test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { + const model = { + ...mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!, + deprecated: true, + max_output_tokens: 2_048, + }; + + expect(shouldSyncNeosantaraModel(model)).toBe(false); + + const built = buildNeosantaraModel({ ...model, deprecated: false }, undefined); + expect(built).toMatchObject({ + base_model: "google/gemini-3.7-flash", + limit: { context: 1_000_000 }, + }); + expect((built as { limit?: { output?: number } }).limit?.output).toBeUndefined(); +}); diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml new file mode 100644 index 00000000000..e301e38f462 --- /dev/null +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -0,0 +1,12 @@ +base_model = "anthropic/claude-opus-4-5" +reasoning = false +structured_output = true + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml new file mode 100644 index 00000000000..40aead7e51b --- /dev/null +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -0,0 +1,12 @@ +base_model = "anthropic/claude-sonnet-4-5" +reasoning = false +structured_output = true + +[cost] +input = 3 +output = 15 +cache_read = 0.3 +cache_write = 3.75 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml new file mode 100644 index 00000000000..51eefc4482b --- /dev/null +++ b/providers/neosantara/models/claude-fable-5.toml @@ -0,0 +1,24 @@ +# Toggle: /v1/messages thinking.type = enabled|disabled +# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) +base_model = "anthropic/claude-fable-5" +attachment = false +structured_output = true + +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "budget_tokens" +min = 1_024 + +[cost] +input = 10 +output = 50 +cache_read = 1 +cache_write = 12.5 + +[limit] +context = 200_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml new file mode 100644 index 00000000000..85f5ed5f269 --- /dev/null +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -0,0 +1,16 @@ +base_model = "anthropic/claude-opus-4-6" +attachment = false +structured_output = true +reasoning_options = [] + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[limit] +context = 200_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml new file mode 100644 index 00000000000..30d6eb25cae --- /dev/null +++ b/providers/neosantara/models/claude-opus-5.toml @@ -0,0 +1,13 @@ +base_model = "anthropic/claude-opus-5" +structured_output = false + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "xhigh"] + +[cost] +input = 5 +output = 25 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-7.toml b/providers/neosantara/models/claude-opus-7.toml new file mode 100644 index 00000000000..c45e26e4c26 --- /dev/null +++ b/providers/neosantara/models/claude-opus-7.toml @@ -0,0 +1,23 @@ +# Toggle: /v1/messages thinking.type = enabled|disabled +# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) +base_model = "anthropic/claude-opus-4-7" +structured_output = true + +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "budget_tokens" +min = 1_024 + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-8.toml b/providers/neosantara/models/claude-opus-8.toml new file mode 100644 index 00000000000..6dd102ab090 --- /dev/null +++ b/providers/neosantara/models/claude-opus-8.toml @@ -0,0 +1,23 @@ +# Toggle: /v1/messages thinking.type = enabled|disabled +# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) +base_model = "anthropic/claude-opus-4-8" +structured_output = true + +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "budget_tokens" +min = 1_024 + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml new file mode 100644 index 00000000000..d1adeebc92c --- /dev/null +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -0,0 +1,16 @@ +base_model = "anthropic/claude-sonnet-4-6" +attachment = false +structured_output = true +reasoning_options = [] + +[cost] +input = 3 +output = 15 +cache_read = 0.3 +cache_write = 3.75 + +[limit] +context = 200_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml new file mode 100644 index 00000000000..e3ca7ee9322 --- /dev/null +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -0,0 +1,23 @@ +# Toggle: /v1/messages thinking.type = enabled|disabled +# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) +base_model = "anthropic/claude-sonnet-5" +structured_output = true + +[[reasoning_options]] +type = "toggle" + +[[reasoning_options]] +type = "budget_tokens" +min = 1_024 + +[cost] +input = 3 +output = 15 +cache_read = 0.3 +cache_write = 3.75 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/deepseek-v4-flash-0731.toml b/providers/neosantara/models/deepseek-v4-flash-0731.toml new file mode 100644 index 00000000000..7e23df01084 --- /dev/null +++ b/providers/neosantara/models/deepseek-v4-flash-0731.toml @@ -0,0 +1,7 @@ +base_model = "deepseek/deepseek-v4-flash-0731" +reasoning = false + +[cost] +input = 0.14 +output = 0.28 +cache_read = 0.03 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml new file mode 100644 index 00000000000..1046147e767 --- /dev/null +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -0,0 +1,12 @@ +base_model = "deepseek/deepseek-v4-flash" +attachment = true +structured_output = false +reasoning_options = [] + +[cost] +input = 0.14 +output = 0.28 +cache_read = 0.0028 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml new file mode 100644 index 00000000000..70b2eced5b1 --- /dev/null +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -0,0 +1,10 @@ +base_model = "deepseek/deepseek-v4-pro-0813" + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] + +[cost] +input = 1.32 +output = 3.96 +cache_read = 0.13 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml new file mode 100644 index 00000000000..8badcd20e35 --- /dev/null +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -0,0 +1,12 @@ +base_model = "deepseek/deepseek-v4-pro" +attachment = true +structured_output = false +reasoning_options = [] + +[cost] +input = 2.4 +output = 4.8 +cache_read = 0.024 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/devstral-2.toml b/providers/neosantara/models/devstral-2.toml new file mode 100644 index 00000000000..0f8527c3250 --- /dev/null +++ b/providers/neosantara/models/devstral-2.toml @@ -0,0 +1,9 @@ +base_model = "mistral/devstral-2512" +structured_output = false + +[cost] +input = 0.4 +output = 2 + +[limit] +context = 256_000 diff --git a/providers/neosantara/models/gemini-3.1-flash-lite.toml b/providers/neosantara/models/gemini-3.1-flash-lite.toml new file mode 100644 index 00000000000..a5bd850a1b3 --- /dev/null +++ b/providers/neosantara/models/gemini-3.1-flash-lite.toml @@ -0,0 +1,13 @@ +base_model = "google/gemini-3.1-flash-lite" +reasoning = false + +[cost] +input = 0.25 +output = 1.5 +cache_read = 0.025 + +[limit] +context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.5-flash.toml b/providers/neosantara/models/gemini-3.5-flash.toml new file mode 100644 index 00000000000..361a3881539 --- /dev/null +++ b/providers/neosantara/models/gemini-3.5-flash.toml @@ -0,0 +1,14 @@ +base_model = "google/gemini-3.5-flash" +reasoning = false + +[cost] +input = 1.5 +output = 9 +cache_read = 0.15 +cache_write = 1.5 + +[limit] +context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml new file mode 100644 index 00000000000..b06b6d4d260 --- /dev/null +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -0,0 +1,14 @@ +base_model = "google/gemini-3.6-flash" +reasoning_options = [] + +[cost] +input = 0.75 +output = 3.75 +cache_read = 0.075 +cache_write = 0.75 + +[limit] +context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml new file mode 100644 index 00000000000..bdb4c487d86 --- /dev/null +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -0,0 +1,14 @@ +base_model = "google/gemini-3.7-flash" +reasoning_options = [] + +[cost] +input = 0.75 +output = 3.75 +cache_read = 0.075 +cache_write = 0.75 + +[limit] +context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml new file mode 100644 index 00000000000..f7cecfcb7a0 --- /dev/null +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -0,0 +1,14 @@ +base_model = "zhipuai/glm-4.5-flash" +attachment = true +reasoning = false +structured_output = false + +[cost] +input = 0.015625 +output = 0.046875 + +[limit] +context = 128_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml new file mode 100644 index 00000000000..07ea5b5f9ab --- /dev/null +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -0,0 +1,10 @@ +# Toggle: thinking.type = enabled (off = field omitted) +base_model = "zhipuai/glm-4.6v-flash" +structured_output = false + +[[reasoning_options]] +type = "toggle" + +[cost] +input = 0.015625 +output = 0.046875 diff --git a/providers/neosantara/models/glm-4.7-flash.toml b/providers/neosantara/models/glm-4.7-flash.toml new file mode 100644 index 00000000000..ddb152274c8 --- /dev/null +++ b/providers/neosantara/models/glm-4.7-flash.toml @@ -0,0 +1,10 @@ +base_model = "zhipuai/glm-4.7-flash" +reasoning = false +structured_output = false + +[cost] +input = 0 +output = 0 + +[limit] +context = 1_000_000 diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml new file mode 100644 index 00000000000..2ee2012297f --- /dev/null +++ b/providers/neosantara/models/glm-4.7.toml @@ -0,0 +1,14 @@ +base_model = "zhipuai/glm-4.7" +attachment = true +reasoning = false +structured_output = false + +[cost] +input = 0.625 +output = 1.875 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml new file mode 100644 index 00000000000..876bdd3ba77 --- /dev/null +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -0,0 +1,14 @@ +base_model = "zhipuai/glm-5.3-flash" +attachment = false + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] + +[cost] +input = 0.15 +output = 0.5 +cache_read = 0.03 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml new file mode 100644 index 00000000000..dccc282e1c8 --- /dev/null +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -0,0 +1,10 @@ +base_model = "openai/gpt-5-nano" +base_model_omit = ["limit.input"] +reasoning_options = [] + +[cost] +input = 0.050625 +output = 0.405313 + +[limit] +context = 128_000 diff --git a/providers/neosantara/models/gpt-5.4-mini.toml b/providers/neosantara/models/gpt-5.4-mini.toml new file mode 100644 index 00000000000..ae8f9b3b063 --- /dev/null +++ b/providers/neosantara/models/gpt-5.4-mini.toml @@ -0,0 +1,16 @@ +base_model = "openai/gpt-5.4-mini" +base_model_omit = ["limit.input"] +attachment = false +reasoning = false + +[cost] +input = 0.75 +output = 4.5 +cache_read = 0.075 +cache_write = 0.75 + +[limit] +context = 128_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml new file mode 100644 index 00000000000..82879e8ddb9 --- /dev/null +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -0,0 +1,6 @@ +base_model = "openai/gpt-5.4-nano" +reasoning_options = [] + +[cost] +input = 0.2025 +output = 1.265625 diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml new file mode 100644 index 00000000000..1df1f655ab4 --- /dev/null +++ b/providers/neosantara/models/gpt-5.4.toml @@ -0,0 +1,15 @@ +base_model = "openai/gpt-5.4" +base_model_omit = ["limit.input"] +attachment = false +reasoning_options = [] + +[cost] +input = 2.5 +output = 15 +cache_read = 0.25 + +[limit] +context = 270_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml new file mode 100644 index 00000000000..ff904d6df19 --- /dev/null +++ b/providers/neosantara/models/gpt-5.5.toml @@ -0,0 +1,15 @@ +base_model = "openai/gpt-5.5" +base_model_omit = ["limit.input"] +attachment = false +reasoning_options = [] + +[cost] +input = 5 +output = 30 +cache_read = 0.5 + +[limit] +context = 270_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml new file mode 100644 index 00000000000..863a3844bbc --- /dev/null +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -0,0 +1,18 @@ +base_model = "openai/gpt-5.6-luna" +base_model_omit = ["limit.input"] +attachment = false +structured_output = false + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] + +[cost] +input = 0.2 +output = 1.2 + +[limit] +context = 272_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml new file mode 100644 index 00000000000..0913c0ec57d --- /dev/null +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -0,0 +1,18 @@ +base_model = "openai/gpt-5.6-sol" +base_model_omit = ["limit.input"] +attachment = false +structured_output = false + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] + +[cost] +input = 5 +output = 30 + +[limit] +context = 272_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml new file mode 100644 index 00000000000..832ad6c65b6 --- /dev/null +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -0,0 +1,17 @@ +base_model = "openai/gpt-5.6-terra" +base_model_omit = ["limit.input"] +structured_output = false + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] + +[cost] +input = 2 +output = 12 + +[limit] +context = 272_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gpt-image-2.toml b/providers/neosantara/models/gpt-image-2.toml new file mode 100644 index 00000000000..1ff78c6e696 --- /dev/null +++ b/providers/neosantara/models/gpt-image-2.toml @@ -0,0 +1 @@ +base_model = "openai/gpt-image-2" diff --git a/providers/neosantara/models/gpt-oss-120b.toml b/providers/neosantara/models/gpt-oss-120b.toml new file mode 100644 index 00000000000..537d744581e --- /dev/null +++ b/providers/neosantara/models/gpt-oss-120b.toml @@ -0,0 +1,7 @@ +base_model = "openai/gpt-oss-120b" +reasoning = false + +[cost] +input = 0.15 +output = 0.6 +cache_read = 0.075 diff --git a/providers/neosantara/models/gpt-oss-20b.toml b/providers/neosantara/models/gpt-oss-20b.toml new file mode 100644 index 00000000000..89f50478552 --- /dev/null +++ b/providers/neosantara/models/gpt-oss-20b.toml @@ -0,0 +1,6 @@ +base_model = "openai/gpt-oss-20b" +reasoning = false + +[cost] +input = 0.025 +output = 0.1 diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml new file mode 100644 index 00000000000..edc30b95a80 --- /dev/null +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -0,0 +1,10 @@ +base_model = "moonshotai/kimi-k2-thinking" +structured_output = false +reasoning_options = [] + +[cost] +input = 0.6 +output = 2.5 + +[limit] +context = 256_000 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml new file mode 100644 index 00000000000..b6de34afe8f --- /dev/null +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -0,0 +1,13 @@ +base_model = "moonshotai/kimi-k2.5" +structured_output = false +reasoning_options = [] + +[cost] +input = 0.6 +output = 3 + +[limit] +context = 256_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml new file mode 100644 index 00000000000..17c0c7e6467 --- /dev/null +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -0,0 +1,10 @@ +base_model = "moonshotai/kimi-k2.6" +attachment = false +reasoning_options = [] + +[cost] +input = 0.285 +output = 1.2 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml new file mode 100644 index 00000000000..8952476f743 --- /dev/null +++ b/providers/neosantara/models/kimi-k3.toml @@ -0,0 +1,11 @@ +base_model = "moonshotai/kimi-k3" +attachment = false +reasoning_options = [] + +[cost] +input = 3 +output = 15 +cache_read = 0.3 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml new file mode 100644 index 00000000000..bf64c0cc244 --- /dev/null +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -0,0 +1,12 @@ +base_model = "poolside/laguna-s-2.1" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0 +output = 0 + +[limit] +context = 262_144 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml new file mode 100644 index 00000000000..bc3a40c06c9 --- /dev/null +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -0,0 +1,9 @@ +base_model = "poolside/laguna-xs-2.1" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0 +output = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml new file mode 100644 index 00000000000..0712b5b99ce --- /dev/null +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -0,0 +1,10 @@ +base_model = "inclusionai/ling-3.0-flash-fin" +structured_output = false +reasoning_options = [] + +[cost] +input = 0 +output = 0 + +[limit] +context = 256_000 diff --git a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml new file mode 100644 index 00000000000..0f540b71f9c --- /dev/null +++ b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml @@ -0,0 +1,10 @@ +base_model = "nvidia/llama-3.3-nemotron-super-49b-v1.5" +reasoning = false +structured_output = true + +[cost] +input = 0.285 +output = 1.999438 + +[limit] +context = 132_000 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml new file mode 100644 index 00000000000..aac9c67fa93 --- /dev/null +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -0,0 +1,7 @@ +base_model = "minimax/MiniMax-M2.7" +structured_output = true +reasoning_options = [] + +[cost] +input = 0.09 +output = 0.36 diff --git a/providers/neosantara/models/minimax-m3.toml b/providers/neosantara/models/minimax-m3.toml new file mode 100644 index 00000000000..1e61f2b5e28 --- /dev/null +++ b/providers/neosantara/models/minimax-m3.toml @@ -0,0 +1,14 @@ +base_model = "minimax/MiniMax-M3" +reasoning = false +structured_output = true + +[cost] +input = 0.3 +output = 1.2 +cache_read = 0.06 + +[limit] +context = 524_288 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/mistral-large-latest.toml b/providers/neosantara/models/mistral-large-latest.toml new file mode 100644 index 00000000000..49a84e572d0 --- /dev/null +++ b/providers/neosantara/models/mistral-large-latest.toml @@ -0,0 +1,13 @@ +base_model = "mistral/mistral-large-latest" +attachment = false +structured_output = true + +[cost] +input = 0.5 +output = 1.5 + +[limit] +context = 128_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/mistral-small-latest.toml b/providers/neosantara/models/mistral-small-latest.toml new file mode 100644 index 00000000000..d2ad2f586af --- /dev/null +++ b/providers/neosantara/models/mistral-small-latest.toml @@ -0,0 +1,14 @@ +base_model = "mistral/mistral-small-latest" +attachment = false +reasoning = false +structured_output = true + +[cost] +input = 0.15 +output = 0.6 + +[limit] +context = 128_000 + +[modalities] +input = ["text"] diff --git a/providers/neosantara/models/muse-glimmer-30b.toml b/providers/neosantara/models/muse-glimmer-30b.toml new file mode 100644 index 00000000000..473053dff46 --- /dev/null +++ b/providers/neosantara/models/muse-glimmer-30b.toml @@ -0,0 +1,7 @@ +base_model = "meta/muse-glimmer-30b" +reasoning = false + +[cost] +input = 0.35 +output = 1.5 +cache_read = 0.04 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml new file mode 100644 index 00000000000..eab9867295e --- /dev/null +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -0,0 +1,11 @@ +base_model = "meta/muse-spark-1.1" +structured_output = false +reasoning_options = [] + +[cost] +input = 1.25 +output = 4.25 +cache_read = 0.15 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml new file mode 100644 index 00000000000..9235cf1a826 --- /dev/null +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -0,0 +1,7 @@ +base_model = "stepfun/step-3.5-flash" +structured_output = true +reasoning_options = [] + +[cost] +input = 0.1875 +output = 0.5625 diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml new file mode 100644 index 00000000000..713d0729255 --- /dev/null +++ b/providers/neosantara/provider.toml @@ -0,0 +1,5 @@ +name = "Neosantara" +env = ["NEOSANTARA_API_KEY"] +npm = "@ai-sdk/openai-compatible" +api = "https://api.neosantara.xyz/v1" +doc = "https://docs.neosantara.xyz" From 1077282f182ca869eb94c53cd6883a494ba8da9a Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 06:30:14 +0000 Subject: [PATCH 02/29] feat(neosantara): add provider logo Single-colour vector mark with a square viewBox and no fixed dimensions, so it inherits the surrounding text colour when inlined. --- providers/neosantara/logo.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 providers/neosantara/logo.svg diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg new file mode 100644 index 00000000000..f58ff01beb8 --- /dev/null +++ b/providers/neosantara/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From ddde6ab3d0d53c12d7cdb5fc968de3b7ad8f9f75 Mon Sep 17 00:00:00 2001 From: Just R Date: Thu, 3 Sep 2026 13:54:35 +0700 Subject: [PATCH 03/29] Update logo --- providers/neosantara/logo.svg | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index f58ff01beb8..64075ae0151 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + Brand Logo + + From a88aa0b59de7bb2e6dc9604138592a8ff0ad0590 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 07:00:41 +0000 Subject: [PATCH 04/29] fix(neosantara): crop logo padding and drop fixed dimensions The embedded raster carried ~13% transparent padding on every side and was inset a further 25 units inside a 500x500 viewBox, so the mark covered only about 66% of the canvas and rendered smaller than neighbouring logos. Crop to the opaque bounds, centre the mark on a square canvas that it fills edge to edge, and drop the root width/height (the site strips them when it inlines the file). Also reduces the file from 102 KB to 59 KB. --- providers/neosantara/logo.svg | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index 64075ae0151..3767200fad3 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -1,12 +1,3 @@ - - - - Brand Logo - + + From b361068e6d9533a7714ca3f45540601b9b464dbd Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 07:22:05 +0000 Subject: [PATCH 05/29] fix(neosantara): author reasoning controls on the host's own field This host is OpenAI-compatible and normalizes reasoning onto a single caller-facing field, reasoning_effort, accepting none|minimal|low|medium|high|xhigh. Upstream-native shapes are handled behind that field and never appear in a caller's request, so documenting a thinking budget or a vendor toggle described the wrong surface. Every reasoning model now authors an effort list taken from its lab entry and same-surface peers, intersected with the values this host accepts, so `max` is dropped and `minimal` appears only where the lab or peers list it. Two models whose lab and peers document no graded level keep a toggle, since on or off is the caller's only choice there. Drops all budget_tokens controls and the empty control sets, and removes the wire comments that pointed at a native surface callers never reach. --- .../core/src/sync/providers/neosantara.ts | 105 ++++++++---------- packages/core/test/neosantara-sync.test.ts | 23 ++-- .../neosantara/models/claude-fable-5.toml | 9 +- .../neosantara/models/claude-opus-4-6.toml | 5 +- .../neosantara/models/claude-opus-7.toml | 9 +- .../neosantara/models/claude-opus-8.toml | 9 +- .../neosantara/models/claude-sonnet-4-6.toml | 5 +- .../neosantara/models/claude-sonnet-5.toml | 9 +- .../neosantara/models/deepseek-v4-flash.toml | 5 +- .../models/deepseek-v4-pro-0813.toml | 2 +- .../neosantara/models/deepseek-v4-pro.toml | 5 +- .../neosantara/models/gemini-3.6-flash.toml | 5 +- .../neosantara/models/gemini-3.7-flash.toml | 5 +- .../neosantara/models/glm-4.6v-flash.toml | 2 +- providers/neosantara/models/gpt-5-nano.toml | 5 +- providers/neosantara/models/gpt-5.4-nano.toml | 5 +- providers/neosantara/models/gpt-5.4.toml | 5 +- providers/neosantara/models/gpt-5.5.toml | 5 +- .../neosantara/models/kimi-k2-thinking.toml | 5 +- providers/neosantara/models/kimi-k2.5.toml | 5 +- providers/neosantara/models/kimi-k2.6.toml | 5 +- providers/neosantara/models/kimi-k3.toml | 5 +- .../neosantara/models/ling-3.0-flash-fin.toml | 5 +- providers/neosantara/models/minimax-m2.7.toml | 5 +- .../neosantara/models/muse-spark-1.1.toml | 5 +- .../neosantara/models/step-3.5-flash.toml | 5 +- 26 files changed, 142 insertions(+), 116 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index f4f188b4998..52c69af78dc 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -135,72 +135,59 @@ type ReasoningControls = NonNullable; /** * Reviewed reasoning controls per model (AGENTS.md → "Reasoning options"). * - * Neosantara is a multi-model relay, so each effort list is the underlying lab/peer set - * intersected with what this host actually accepts and passes on. The public request schema - * takes `reasoning_effort` of none|minimal|low|medium|high|xhigh only, so peer values such as - * `max` are dropped, and `minimal` is never added unless the lab/peers list it. `[]` means the - * model reasons but exposes no caller control on this host. + * This host is OpenAI-compatible and normalizes reasoning onto a single caller-facing + * field, `reasoning_effort`, accepting none|minimal|low|medium|high|xhigh. Upstream-native + * shapes (thinking budgets, vendor toggles) are handled behind that field and never appear + * in a caller's request, so every entry below is an effort list taken from the underlying + * lab entry and its same-surface peers, intersected with the values this host accepts — + * which is why `max` never appears. * - * Every entry was verified against this host's public request surface. Internal routing is - * deliberately not described here. + * The exception is a model whose lab and peers document no graded level at all: there the + * only caller-visible choice is on or off, which is a toggle. */ -const EFFORT_GPT_5_6: ReasoningControls = [ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, -]; -const EFFORT_LOW_HIGH: ReasoningControls = [{ type: "effort", values: ["low", "high"] }]; -const EFFORT_LOW_MEDIUM_HIGH: ReasoningControls = [ - { type: "effort", values: ["low", "medium", "high"] }, -]; -const THINKING_BUDGET: ReasoningControls = [ - { type: "toggle" }, - { type: "budget_tokens", min: 1_024 }, -]; -const THINKING_BUDGET_HEADER = - "# Toggle: /v1/messages thinking.type = enabled|disabled\n# Budget: /v1/messages thinking.budget_tokens (reasoning tokens)\n"; -const NO_CALLER_CONTROL: ReasoningControls = []; +const effort = (...values: string[]): ReasoningControls => + [{ type: "effort", values: values as never }]; const REASONING_CONTROLS: Record = { - // `reasoning_effort` is accepted and honoured for these models. - "gpt-5.6-sol": { options: EFFORT_GPT_5_6 }, - "gpt-5.6-terra": { options: EFFORT_GPT_5_6 }, - "gpt-5.6-luna": { options: EFFORT_GPT_5_6 }, - "claude-opus-5": { options: [{ type: "effort", values: ["low", "medium", "high", "xhigh"] }] }, - // Graded effort is honoured, but only the levels the lab and its peers document. - "deepseek-v4-pro-0813": { options: EFFORT_LOW_HIGH }, - "glm-5.3-flash": { options: EFFORT_LOW_HIGH }, - // Peers are split: several author a toggle this host cannot express, so the effort list - // follows the same-surface effort peers (pioneer, opencode). - "laguna-s-2.1": { options: EFFORT_LOW_MEDIUM_HIGH }, - "laguna-xs-2.1": { options: EFFORT_LOW_MEDIUM_HIGH }, - // Thinking is a plain on/off here; no graded effort reaches the model. + "claude-fable-5": { options: effort("low", "medium", "high", "xhigh") }, + "claude-opus-4-6": { options: effort("low", "medium", "high") }, + "claude-opus-5": { options: effort("low", "medium", "high", "xhigh") }, + "claude-opus-7": { options: effort("low", "medium", "high", "xhigh") }, + "claude-opus-8": { options: effort("low", "medium", "high", "xhigh") }, + "claude-sonnet-4-6": { options: effort("low", "medium", "high") }, + "claude-sonnet-5": { options: effort("low", "medium", "high", "xhigh") }, + "deepseek-v4-flash": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, + "deepseek-v4-pro": { options: effort("high") }, + "deepseek-v4-pro-0813": { options: effort("high") }, + "gemini-3.6-flash": { options: effort("minimal", "low", "medium", "high") }, + "gemini-3.7-flash": { options: effort("low", "medium", "high") }, + "glm-5.3-flash": { options: effort("low", "high") }, + "gpt-5-nano": { options: effort("minimal", "low", "medium", "high") }, + "gpt-5.4": { options: effort("none", "low", "medium", "high", "xhigh") }, + "gpt-5.4-nano": { options: effort("none", "low", "medium", "high", "xhigh") }, + "gpt-5.5": { options: effort("none", "low", "medium", "high", "xhigh") }, + "gpt-5.6-luna": { options: effort("none", "low", "medium", "high", "xhigh") }, + "gpt-5.6-sol": { options: effort("none", "low", "medium", "high", "xhigh") }, + "gpt-5.6-terra": { options: effort("none", "low", "medium", "high", "xhigh") }, + "kimi-k2-thinking": { options: effort("high") }, + "kimi-k2.5": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, + "kimi-k2.6": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, + "kimi-k3": { options: effort("low", "high") }, + "laguna-s-2.1": { options: effort("low", "medium", "high") }, + "laguna-xs-2.1": { options: effort("low", "medium", "high") }, + "minimax-m2.7": { options: effort("low", "medium", "high", "xhigh") }, + "muse-spark-1.1": { options: effort("minimal", "low", "medium", "high", "xhigh") }, + "step-3.5-flash": { options: effort("low", "high") }, + // No lab or peer entry documents a graded level for these, so the caller's only + // choice is whether to reason at all. "glm-4.6v-flash": { options: [{ type: "toggle" }], - header: "# Toggle: thinking.type = enabled (off = field omitted)\n", + header: "# Toggle: reasoning_effort = none turns reasoning off\n", + }, + "ling-3.0-flash-fin": { + options: [{ type: "toggle" }], + header: "# Toggle: reasoning_effort = none turns reasoning off\n", }, - // Graded effort is not honoured for these; the caller steers them with a thinking budget. - "claude-fable-5": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, - "claude-sonnet-5": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, - "claude-opus-7": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, - "claude-opus-8": { options: THINKING_BUDGET, header: THINKING_BUDGET_HEADER }, - // Models that reason but accept no reasoning control on this host. - "claude-sonnet-4-6": { options: NO_CALLER_CONTROL }, - "claude-opus-4-6": { options: NO_CALLER_CONTROL }, - "deepseek-v4-flash": { options: NO_CALLER_CONTROL }, - "deepseek-v4-pro": { options: NO_CALLER_CONTROL }, - "gemini-3.6-flash": { options: NO_CALLER_CONTROL }, - "gemini-3.7-flash": { options: NO_CALLER_CONTROL }, - "gpt-5-nano": { options: NO_CALLER_CONTROL }, - "gpt-5.4": { options: NO_CALLER_CONTROL }, - "gpt-5.4-nano": { options: NO_CALLER_CONTROL }, - "gpt-5.5": { options: NO_CALLER_CONTROL }, - "kimi-k2-thinking": { options: NO_CALLER_CONTROL }, - "kimi-k2.5": { options: NO_CALLER_CONTROL }, - "kimi-k2.6": { options: NO_CALLER_CONTROL }, - "kimi-k3": { options: NO_CALLER_CONTROL }, - "ling-3.0-flash-fin": { options: NO_CALLER_CONTROL }, - "minimax-m2.7": { options: NO_CALLER_CONTROL }, - "muse-spark-1.1": { options: NO_CALLER_CONTROL }, - "step-3.5-flash": { options: NO_CALLER_CONTROL }, }; /** Image models are priced per image and carry no context window, so they skip the token filters. */ diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index f77b9157105..615ce964d34 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -136,7 +136,7 @@ test("builds override-only models with effective USD pricing and host reasoning const gemini = buildNeosantaraModel(merged[0]!, undefined); expect(gemini).toMatchObject({ base_model: "google/gemini-3.7-flash", - reasoning_options: [], + reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }], limit: { context: 1_000_000 }, cost: { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 }, }); @@ -162,19 +162,24 @@ test("authors reviewed per-host reasoning controls instead of dumping the reques expect(build("claude-opus-5").reasoning_options).toEqual([ { type: "effort", values: ["low", "medium", "high", "xhigh"] }, ]); + // Native upstream shapes never reach a caller: the host normalizes everything onto + // reasoning_effort, so these are effort lists, not thinking budgets. expect(build("claude-fable-5").reasoning_options).toEqual([ - { type: "toggle" }, - { type: "budget_tokens", min: 1_024 }, + { type: "effort", values: ["low", "medium", "high", "xhigh"] }, + ]); + expect(build("kimi-k2-thinking").reasoning_options).toEqual([ + { type: "effort", values: ["high"] }, + ]); + expect(build("gpt-5.4").reasoning_options).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, ]); + // Only models whose lab and peers document no graded level stay a plain on/off. expect(build("glm-4.6v-flash").reasoning_options).toEqual([{ type: "toggle" }]); - expect(build("kimi-k2-thinking").reasoning_options).toEqual([]); - expect(build("gpt-5.4").reasoning_options).toEqual([]); for (const id of ["gpt-5.6-terra", "claude-opus-5", "glm-5.3-flash", "laguna-s-2.1"]) { - const values = build(id).reasoning_options?.flatMap((option) => - "values" in option ? option.values : [], - ); - expect(values).not.toContain("minimal"); + const options = build(id).reasoning_options ?? []; + expect(options.some((option) => option.type === "budget_tokens")).toBe(false); + const values = options.flatMap((option) => ("values" in option ? option.values : [])); expect(values).not.toContain("max"); } }); diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 51eefc4482b..32ead780e21 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -1,15 +1,10 @@ -# Toggle: /v1/messages thinking.type = enabled|disabled -# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) base_model = "anthropic/claude-fable-5" attachment = false structured_output = true [[reasoning_options]] -type = "toggle" - -[[reasoning_options]] -type = "budget_tokens" -min = 1_024 +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 85f5ed5f269..9041faaa550 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,7 +1,10 @@ base_model = "anthropic/claude-opus-4-6" attachment = false structured_output = true -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-7.toml b/providers/neosantara/models/claude-opus-7.toml index c45e26e4c26..c72a6f60cb2 100644 --- a/providers/neosantara/models/claude-opus-7.toml +++ b/providers/neosantara/models/claude-opus-7.toml @@ -1,14 +1,9 @@ -# Toggle: /v1/messages thinking.type = enabled|disabled -# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) base_model = "anthropic/claude-opus-4-7" structured_output = true [[reasoning_options]] -type = "toggle" - -[[reasoning_options]] -type = "budget_tokens" -min = 1_024 +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-8.toml b/providers/neosantara/models/claude-opus-8.toml index 6dd102ab090..e6bc340d309 100644 --- a/providers/neosantara/models/claude-opus-8.toml +++ b/providers/neosantara/models/claude-opus-8.toml @@ -1,14 +1,9 @@ -# Toggle: /v1/messages thinking.type = enabled|disabled -# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) base_model = "anthropic/claude-opus-4-8" structured_output = true [[reasoning_options]] -type = "toggle" - -[[reasoning_options]] -type = "budget_tokens" -min = 1_024 +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index d1adeebc92c..a92d02f8b48 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,7 +1,10 @@ base_model = "anthropic/claude-sonnet-4-6" attachment = false structured_output = true -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index e3ca7ee9322..7171723b679 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,14 +1,9 @@ -# Toggle: /v1/messages thinking.type = enabled|disabled -# Budget: /v1/messages thinking.budget_tokens (reasoning tokens) base_model = "anthropic/claude-sonnet-5" structured_output = true [[reasoning_options]] -type = "toggle" - -[[reasoning_options]] -type = "budget_tokens" -min = 1_024 +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 1046147e767..ab7092d46cc 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,7 +1,10 @@ base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 70b2eced5b1..6316316d068 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -2,7 +2,7 @@ base_model = "deepseek/deepseek-v4-pro-0813" [[reasoning_options]] type = "effort" -values = ["low", "high"] +values = ["high"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 8badcd20e35..26df780c415 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,7 +1,10 @@ base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["high"] [cost] input = 2.4 diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index b06b6d4d260..85e08c1c2c3 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -1,5 +1,8 @@ base_model = "google/gemini-3.6-flash" -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] [cost] input = 0.75 diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index bdb4c487d86..5e0a2dadbec 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -1,5 +1,8 @@ base_model = "google/gemini-3.7-flash" -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 0.75 diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 07ea5b5f9ab..81b01d4c25c 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,4 +1,4 @@ -# Toggle: thinking.type = enabled (off = field omitted) +# Toggle: reasoning_effort = none turns reasoning off base_model = "zhipuai/glm-4.6v-flash" structured_output = false diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index dccc282e1c8..01b8623f3d0 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -1,6 +1,9 @@ base_model = "openai/gpt-5-nano" base_model_omit = ["limit.input"] -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] [cost] input = 0.050625 diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index 82879e8ddb9..1fb8800ae75 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -1,5 +1,8 @@ base_model = "openai/gpt-5.4-nano" -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 0.2025 diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 1df1f655ab4..20eaba88c87 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -1,7 +1,10 @@ base_model = "openai/gpt-5.4" base_model_omit = ["limit.input"] attachment = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 2.5 diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index ff904d6df19..b2faccff55a 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -1,7 +1,10 @@ base_model = "openai/gpt-5.5" base_model_omit = ["limit.input"] attachment = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index edc30b95a80..0dd5f96985f 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,6 +1,9 @@ base_model = "moonshotai/kimi-k2-thinking" structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["high"] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index b6de34afe8f..4a120003a3f 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,6 +1,9 @@ base_model = "moonshotai/kimi-k2.5" structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 17c0c7e6467..6e7ad67ae60 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,6 +1,9 @@ base_model = "moonshotai/kimi-k2.6" attachment = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.285 diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 8952476f743..9484f9c3027 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,6 +1,9 @@ base_model = "moonshotai/kimi-k3" attachment = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] [cost] input = 3 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 0712b5b99ce..97c2e1cb873 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,6 +1,9 @@ +# Toggle: reasoning_effort = none turns reasoning off base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index aac9c67fa93..a40fcb7b232 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,6 +1,9 @@ base_model = "minimax/MiniMax-M2.7" structured_output = true -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 0.09 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index eab9867295e..98d05e03b0d 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -1,6 +1,9 @@ base_model = "meta/muse-spark-1.1" structured_output = false -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high", "xhigh"] [cost] input = 1.25 diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 9235cf1a826..8891addb422 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,6 +1,9 @@ base_model = "stepfun/step-3.5-flash" structured_output = true -reasoning_options = [] + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] [cost] input = 0.1875 From dd7b6ded160fbf163c1ee907df4dbdb9508199b2 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 07:25:20 +0000 Subject: [PATCH 06/29] refactor(neosantara): derive reasoning controls from the canonical tree The per-model table meant every new reasoning model needed a code change. The values it held were already in the repository, so read them at sync time instead: the underlying lab entry wins, otherwise the set its same-surface peers agree on, intersected with the values this host accepts. Preferring the lab over a relay also corrects two entries the table had copied from the wrong source. deepseek-v4-flash now follows DeepSeek's own low|high rather than a relay's full enum, and laguna-xs-2.1 is a toggle like Poolside's own entry instead of inheriting its sibling's graded levels. New models now inherit controls with no change here. --- .../core/src/sync/providers/neosantara.ts | 166 ++++++++++++------ packages/core/test/neosantara-sync.test.ts | 46 +++-- .../neosantara/models/deepseek-v4-flash.toml | 2 +- .../neosantara/models/laguna-xs-2.1.toml | 4 +- 4 files changed, 149 insertions(+), 69 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 52c69af78dc..b180f8d3f49 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -1,3 +1,6 @@ +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; + import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; @@ -5,6 +8,7 @@ import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js" const MODELS_ENDPOINT = "https://api.neosantara.xyz/v1/models"; const PRICING_ENDPOINT = "https://api.neosantara.xyz/v1/public/pricing"; const MIN_CONTEXT_WINDOW = 100_000; +const PROVIDERS_DIR = path.join(process.cwd(), "providers"); const Pricing = z.object({ currency: z.enum(["USD", "IDR"]), @@ -133,62 +137,110 @@ export function mergeNeosantaraCatalogs( type ReasoningControls = NonNullable; /** - * Reviewed reasoning controls per model (AGENTS.md → "Reasoning options"). + * Reasoning controls (AGENTS.md → "Reasoning options"). * * This host is OpenAI-compatible and normalizes reasoning onto a single caller-facing - * field, `reasoning_effort`, accepting none|minimal|low|medium|high|xhigh. Upstream-native - * shapes (thinking budgets, vendor toggles) are handled behind that field and never appear - * in a caller's request, so every entry below is an effort list taken from the underlying - * lab entry and its same-surface peers, intersected with the values this host accepts — - * which is why `max` never appears. + * field, `reasoning_effort`, accepting the values below. Upstream-native shapes (thinking + * budgets, vendor toggles) are handled behind that field and never appear in a caller's + * request, so the control is always an effort list. * - * The exception is a model whose lab and peers document no graded level at all: there the - * only caller-visible choice is on or off, which is a toggle. + * The values are read from the canonical tree at sync time rather than hardcoded: the + * underlying lab entry wins, otherwise the set its same-surface peers agree on, and the + * result is intersected with what this host accepts — which is why `max` never appears. + * A model whose lab and peers document no graded level at all has only an on/off choice, + * which is a toggle. New models therefore need no change here. */ -const effort = (...values: string[]): ReasoningControls => - [{ type: "effort", values: values as never }]; - -const REASONING_CONTROLS: Record = { - "claude-fable-5": { options: effort("low", "medium", "high", "xhigh") }, - "claude-opus-4-6": { options: effort("low", "medium", "high") }, - "claude-opus-5": { options: effort("low", "medium", "high", "xhigh") }, - "claude-opus-7": { options: effort("low", "medium", "high", "xhigh") }, - "claude-opus-8": { options: effort("low", "medium", "high", "xhigh") }, - "claude-sonnet-4-6": { options: effort("low", "medium", "high") }, - "claude-sonnet-5": { options: effort("low", "medium", "high", "xhigh") }, - "deepseek-v4-flash": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, - "deepseek-v4-pro": { options: effort("high") }, - "deepseek-v4-pro-0813": { options: effort("high") }, - "gemini-3.6-flash": { options: effort("minimal", "low", "medium", "high") }, - "gemini-3.7-flash": { options: effort("low", "medium", "high") }, - "glm-5.3-flash": { options: effort("low", "high") }, - "gpt-5-nano": { options: effort("minimal", "low", "medium", "high") }, - "gpt-5.4": { options: effort("none", "low", "medium", "high", "xhigh") }, - "gpt-5.4-nano": { options: effort("none", "low", "medium", "high", "xhigh") }, - "gpt-5.5": { options: effort("none", "low", "medium", "high", "xhigh") }, - "gpt-5.6-luna": { options: effort("none", "low", "medium", "high", "xhigh") }, - "gpt-5.6-sol": { options: effort("none", "low", "medium", "high", "xhigh") }, - "gpt-5.6-terra": { options: effort("none", "low", "medium", "high", "xhigh") }, - "kimi-k2-thinking": { options: effort("high") }, - "kimi-k2.5": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, - "kimi-k2.6": { options: effort("none", "minimal", "low", "medium", "high", "xhigh") }, - "kimi-k3": { options: effort("low", "high") }, - "laguna-s-2.1": { options: effort("low", "medium", "high") }, - "laguna-xs-2.1": { options: effort("low", "medium", "high") }, - "minimax-m2.7": { options: effort("low", "medium", "high", "xhigh") }, - "muse-spark-1.1": { options: effort("minimal", "low", "medium", "high", "xhigh") }, - "step-3.5-flash": { options: effort("low", "high") }, - // No lab or peer entry documents a graded level for these, so the caller's only - // choice is whether to reason at all. - "glm-4.6v-flash": { - options: [{ type: "toggle" }], - header: "# Toggle: reasoning_effort = none turns reasoning off\n", - }, - "ling-3.0-flash-fin": { - options: [{ type: "toggle" }], - header: "# Toggle: reasoning_effort = none turns reasoning off\n", - }, -}; +const HOST_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]); + +const TOGGLE_HEADER = "# Toggle: reasoning_effort = none turns reasoning off\n"; + +function tomlFilesIn(dir: string): string[] { + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return []; + } + return entries.flatMap((entry) => + entry.isDirectory() + ? tomlFilesIn(path.join(dir, entry.name)) + : entry.name.endsWith(".toml") + ? [path.join(dir, entry.name)] + : [], + ); +} + +function parseToml(filePath: string) { + try { + return Bun.TOML.parse(readFileSync(filePath, "utf8")) as Record; + } catch { + return undefined; + } +} + +/** Effort values declared by a provider file, restricted to what this host can send. */ +function hostEfforts(options: unknown) { + if (!Array.isArray(options)) return undefined; + for (const option of options) { + if (typeof option !== "object" || option === null) continue; + if ((option as { type?: unknown }).type !== "effort") continue; + const values = (option as { values?: unknown }).values; + if (!Array.isArray(values)) continue; + const accepted = values.filter( + (value): value is string => typeof value === "string" && HOST_EFFORTS.has(value), + ); + if (accepted.length > 0) return accepted; + } + return undefined; +} + +let effortsByBaseModel: Map | undefined; + +/** Effort set each base model's peers agree on, most common wins. */ +function peerEfforts() { + if (effortsByBaseModel !== undefined) return effortsByBaseModel; + + const tally = new Map>(); + for (const file of tomlFilesIn(PROVIDERS_DIR)) { + if (file.startsWith(path.join(PROVIDERS_DIR, "neosantara"))) continue; + const toml = parseToml(file); + const base = toml?.base_model; + if (typeof base !== "string") continue; + const values = hostEfforts(toml?.reasoning_options); + if (values === undefined) continue; + const counts = tally.get(base) ?? new Map(); + const key = values.join(","); + counts.set(key, (counts.get(key) ?? 0) + 1); + tally.set(base, counts); + } + + effortsByBaseModel = new Map(); + for (const [base, counts] of tally) { + const [best] = [...counts.entries()].sort((a, b) => b[1] - a[1]); + if (best !== undefined) effortsByBaseModel.set(base, best[0].split(",")); + } + return effortsByBaseModel; +} + +/** The lab's own entry for a model, which outranks any relay. */ +function firstPartyEfforts(baseModel: string) { + const [lab, ...rest] = baseModel.split("/"); + if (lab === undefined || rest.length === 0) return undefined; + return hostEfforts( + parseToml(path.join(PROVIDERS_DIR, lab, "models", `${rest.join("/")}.toml`))?.reasoning_options, + ); +} + +export function neosantaraReasoningControls(id: string, baseModel: string): ReasoningControls { + const derived = firstPartyEfforts(baseModel) ?? peerEfforts().get(baseModel); + return derived === undefined + ? [{ type: "toggle" }] + : [{ type: "effort", values: derived as never }]; +} + +function reasoningHeader(controls: ReasoningControls) { + return controls.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; +} /** Image models are priced per image and carry no context window, so they skip the token filters. */ function isImageModel(model: NeosantaraSourceModel) { @@ -216,7 +268,7 @@ export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { if (isImageModel(model)) return true; // Skip rather than throw: one missing catalog field must cost a single model, not the run. if (model.pricing.prompt === undefined || model.pricing.completion === undefined) return false; - return !model.capabilities.includes("reasoning") || REASONING_CONTROLS[model.id] !== undefined; + return true; } function effectiveUsd(value: number | undefined, model: NeosantaraSourceModel) { @@ -277,7 +329,7 @@ export function buildNeosantaraModel( { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoning ? REASONING_CONTROLS[model.id]?.options ?? [] : undefined, + reasoning_options: reasoning ? neosantaraReasoningControls(model.id, baseModel) : undefined, attachment: model.capabilities.includes("vision"), tool_call: true, structured_output: model.capabilities.includes("json_mode"), @@ -340,7 +392,11 @@ export const neosantara = { return { id: model.id, model: buildNeosantaraModel(model, context.existing(model.id)), - header: REASONING_CONTROLS[model.id]?.header, + header: reasoningHeader( + model.capabilities.includes("reasoning") + ? neosantaraReasoningControls(model.id, resolveNeosantaraBaseModel(model.id) ?? "") + : [], + ), }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 615ce964d34..e76d3367e4b 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -5,6 +5,7 @@ import { mergeNeosantaraCatalogs, neosantara, neosantaraInputModalities, + neosantaraReasoningControls, NeosantaraModelsResponse, NeosantaraPricingResponse, resolveNeosantaraBaseModel, @@ -184,19 +185,19 @@ test("authors reviewed per-host reasoning controls instead of dumping the reques } }); -test("refuses to sync a reasoning model that has no reviewed control set", () => { +test("syncs a reasoning model with no per-model configuration in the module", () => { const merged = mergeNeosantaraCatalogs( NeosantaraModelsResponse.parse(modelsResponse), NeosantaraPricingResponse.parse(pricingResponse), ); - const unreviewed = { + const reasoner = { ...merged[1]!, capabilities: [...merged[1]!.capabilities, "reasoning"], }; - expect(shouldSyncNeosantaraModel(merged[1]!)).toBe(true); - expect(shouldSyncNeosantaraModel(unreviewed)).toBe(false); + expect(shouldSyncNeosantaraModel(reasoner)).toBe(true); + expect(buildNeosantaraModel(reasoner, undefined).reasoning_options?.length).toBeGreaterThan(0); }); test("keeps model pricing when the public pricing entry is only partially populated", () => { @@ -410,19 +411,16 @@ test("resolves base models from the canonical metadata tree, aliasing only ambig expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); }); -test("reports reasoning models held back for control review instead of dropping them silently", () => { +test("reports models skipped for want of a canonical entry", () => { const merged = mergeNeosantaraCatalogs( NeosantaraModelsResponse.parse(modelsResponse), NeosantaraPricingResponse.parse(pricingResponse), ); - const unreviewed = { - ...merged[1]!, - capabilities: [...merged[1]!.capabilities, "reasoning"], - }; + const unknown = { ...merged[1]!, id: "not-in-the-canonical-tree" }; - expect(shouldSyncNeosantaraModel(unreviewed)).toBe(false); - expect(neosantara.sourceID(unreviewed)).toBe("gpt-oss-20b"); + expect(shouldSyncNeosantaraModel(unknown)).toBe(false); + expect(neosantara.sourceID(unknown)).toBe("not-in-the-canonical-tree"); }); test("surfaces skipped models in the sync notice so they are not collected and discarded", () => { @@ -459,6 +457,32 @@ test("skips a model with unusable upstream pricing instead of aborting the whole expect(neosantara.sourceID(merged[0]!)).toBe("kimi-k2.5"); }); +test("derives reasoning controls from the canonical tree so new models need no code change", () => { + // Nothing about these ids is listed in the module; the values come from the lab entry + // and its same-surface peers, intersected with what this host accepts. + expect(neosantaraReasoningControls("gpt-5.6-terra", "openai/gpt-5.6-terra")).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); + expect(neosantaraReasoningControls("claude-fable-5", "anthropic/claude-fable-5")).toEqual([ + { type: "effort", values: ["low", "medium", "high", "xhigh"] }, + ]); + expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) + .toEqual([{ type: "effort", values: ["high"] }]); + expect(neosantaraReasoningControls("gemini-3.7-flash", "google/gemini-3.7-flash")).toEqual([ + { type: "effort", values: ["low", "medium", "high"] }, + ]); + + // No lab or peer entry documents a graded level for this one. + expect(neosantaraReasoningControls("glm-4.6v-flash", "zhipuai/glm-4.6v-flash")).toEqual([ + { type: "toggle" }, + ]); + + // A model nobody has authored yet still resolves to something usable. + expect(neosantaraReasoningControls("brand-new", "openai/gpt-5.6-sol")).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); +}); + test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { const model = { ...mergeNeosantaraCatalogs( diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index ab7092d46cc..d0815ef86b0 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -4,7 +4,7 @@ structured_output = false [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["low", "high"] [cost] input = 0.14 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index bc3a40c06c9..a4ab563b064 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,8 +1,8 @@ +# Toggle: reasoning_effort = none turns reasoning off base_model = "poolside/laguna-xs-2.1" [[reasoning_options]] -type = "effort" -values = ["low", "medium", "high"] +type = "toggle" [cost] input = 0 From 28e344387422234eb0f94d65a24d6c5298e60d22 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 07:38:05 +0000 Subject: [PATCH 07/29] fix(neosantara): follow the lab's own controls when deriving reasoning options The first pass at deriving controls had four flaws. It looked for a lab entry at one fixed path, so nested layouts such as providers/poolside/models/poolside were missed; it ignored lab entries that declare no control, so an always-on reasoner borrowed a minority peer's levels; it tallied only peers that already had an effort list, letting one relay outvote thirty toggles; and it fell back to a bare toggle, which claims an on/off field this host does not expose. Controls are now projected onto this host's single field the way Eden AI does it: an empty lab set stays empty, a lab toggle becomes reasoning_effort = none and joins the effort list, and the lab always outranks a relay. kimi-k2-thinking is always-on again, kimi-k2.5 and k2.6 no longer claim graded levels their lab does not document, deepseek-v4-pro gains the none it needs to turn thinking off, and the Laguna pair follows Poolside instead of a sibling. --- .../core/src/sync/providers/neosantara.ts | 100 +++++++++++------- packages/core/test/neosantara-sync.test.ts | 82 +++++++------- .../neosantara/models/claude-sonnet-5.toml | 2 +- .../neosantara/models/deepseek-v4-flash.toml | 2 +- .../models/deepseek-v4-pro-0813.toml | 2 +- .../neosantara/models/deepseek-v4-pro.toml | 2 +- .../neosantara/models/glm-4.6v-flash.toml | 4 +- .../neosantara/models/kimi-k2-thinking.toml | 5 +- providers/neosantara/models/kimi-k2.5.toml | 2 +- providers/neosantara/models/kimi-k2.6.toml | 2 +- providers/neosantara/models/kimi-k3.toml | 2 +- providers/neosantara/models/laguna-s-2.1.toml | 2 +- .../neosantara/models/laguna-xs-2.1.toml | 4 +- .../neosantara/models/ling-3.0-flash-fin.toml | 4 +- providers/neosantara/models/minimax-m2.7.toml | 5 +- 15 files changed, 120 insertions(+), 100 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index b180f8d3f49..61ee9f28c4d 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -152,7 +152,8 @@ type ReasoningControls = NonNullable; */ const HOST_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]); -const TOGGLE_HEADER = "# Toggle: reasoning_effort = none turns reasoning off\n"; +/** A model that reasons with no caller control. */ +const ALWAYS_ON = "always-on"; function tomlFilesIn(dir: string): string[] { let entries; @@ -178,68 +179,94 @@ function parseToml(filePath: string) { } } -/** Effort values declared by a provider file, restricted to what this host can send. */ -function hostEfforts(options: unknown) { +/** + * Project another entry's controls onto this host's single field. + * + * An empty set means always-on. A lab-side toggle is `reasoning_effort = none` here, so it + * joins the effort list rather than staying a separate option — a bare toggle would claim an + * on/off field this host does not have. Values the host cannot send are dropped. + */ +function hostControls(options: unknown): string[] | typeof ALWAYS_ON | undefined { if (!Array.isArray(options)) return undefined; + if (options.length === 0) return ALWAYS_ON; + + let toggled = false; + let accepted: string[] | undefined; for (const option of options) { if (typeof option !== "object" || option === null) continue; - if ((option as { type?: unknown }).type !== "effort") continue; + const type = (option as { type?: unknown }).type; + if (type === "toggle") toggled = true; + if (type !== "effort") continue; const values = (option as { values?: unknown }).values; if (!Array.isArray(values)) continue; - const accepted = values.filter( + const usable = values.filter( (value): value is string => typeof value === "string" && HOST_EFFORTS.has(value), ); - if (accepted.length > 0) return accepted; + if (usable.length > 0) accepted = usable; } - return undefined; + + if (accepted === undefined) return toggled ? ["none"] : undefined; + return toggled && !accepted.includes("none") ? ["none", ...accepted] : accepted; } -let effortsByBaseModel: Map | undefined; +let controlIndex: { lab: Map>; peers: Map> } | undefined; -/** Effort set each base model's peers agree on, most common wins. */ -function peerEfforts() { - if (effortsByBaseModel !== undefined) return effortsByBaseModel; +/** Index every other provider's controls: the model's own lab, then peer consensus. */ +function indexControls() { + if (controlIndex !== undefined) return controlIndex; + const lab = new Map>(); const tally = new Map>(); + const shapes = new Map>>(); + for (const file of tomlFilesIn(PROVIDERS_DIR)) { if (file.startsWith(path.join(PROVIDERS_DIR, "neosantara"))) continue; const toml = parseToml(file); - const base = toml?.base_model; - if (typeof base !== "string") continue; - const values = hostEfforts(toml?.reasoning_options); - if (values === undefined) continue; + if (toml === undefined) continue; + const controls = hostControls(toml.reasoning_options); + + // A lab's own directory is authoritative, wherever the file sits inside it. + const owner = path.relative(PROVIDERS_DIR, file).split(path.sep)[0]; + const name = path.basename(file, ".toml"); + if (owner !== undefined && !lab.has(`${owner}/${name}`)) lab.set(`${owner}/${name}`, controls); + + const base = toml.base_model; + if (typeof base !== "string" || controls === undefined) continue; + const key = controls === ALWAYS_ON ? ALWAYS_ON : controls.join(","); const counts = tally.get(base) ?? new Map(); - const key = values.join(","); counts.set(key, (counts.get(key) ?? 0) + 1); tally.set(base, counts); + const seen = shapes.get(base) ?? new Map(); + seen.set(key, controls); + shapes.set(base, seen); } - effortsByBaseModel = new Map(); + const peers = new Map>(); for (const [base, counts] of tally) { const [best] = [...counts.entries()].sort((a, b) => b[1] - a[1]); - if (best !== undefined) effortsByBaseModel.set(base, best[0].split(",")); + if (best !== undefined) peers.set(base, shapes.get(base)?.get(best[0])); } - return effortsByBaseModel; -} -/** The lab's own entry for a model, which outranks any relay. */ -function firstPartyEfforts(baseModel: string) { - const [lab, ...rest] = baseModel.split("/"); - if (lab === undefined || rest.length === 0) return undefined; - return hostEfforts( - parseToml(path.join(PROVIDERS_DIR, lab, "models", `${rest.join("/")}.toml`))?.reasoning_options, - ); + controlIndex = { lab, peers }; + return controlIndex; } +/** + * Reasoning controls (AGENTS.md → "Reasoning options"). + * + * This host is OpenAI-compatible and normalizes reasoning onto one caller-facing field, + * `reasoning_effort`. Upstream-native shapes never reach a caller, so controls are read from + * the canonical tree at sync time instead of being listed here: the model's own lab entry + * wins, otherwise the shape its peers agree on. New models need no change here. + */ export function neosantaraReasoningControls(id: string, baseModel: string): ReasoningControls { - const derived = firstPartyEfforts(baseModel) ?? peerEfforts().get(baseModel); - return derived === undefined - ? [{ type: "toggle" }] - : [{ type: "effort", values: derived as never }]; -} + const { lab, peers } = indexControls(); + const [owner, ...rest] = baseModel.split("/"); + const key = `${owner}/${rest.at(-1)}`; + const derived = lab.has(key) ? lab.get(key) : peers.get(baseModel); -function reasoningHeader(controls: ReasoningControls) { - return controls.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; + if (derived === undefined || derived === ALWAYS_ON) return []; + return [{ type: "effort", values: derived as never }]; } /** Image models are priced per image and carry no context window, so they skip the token filters. */ @@ -392,11 +419,6 @@ export const neosantara = { return { id: model.id, model: buildNeosantaraModel(model, context.existing(model.id)), - header: reasoningHeader( - model.capabilities.includes("reasoning") - ? neosantaraReasoningControls(model.id, resolveNeosantaraBaseModel(model.id) ?? "") - : [], - ), }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index e76d3367e4b..8540529fb72 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -150,39 +150,35 @@ test("builds override-only models with effective USD pricing and host reasoning }); }); -test("authors reviewed per-host reasoning controls instead of dumping the request enum", () => { +test("never advertises a control this host cannot send", () => { const source = mergeNeosantaraCatalogs( NeosantaraModelsResponse.parse(modelsResponse), NeosantaraPricingResponse.parse(pricingResponse), )[0]!; - const build = (id: string) => buildNeosantaraModel({ ...source, id }, undefined); - expect(build("gpt-5.6-terra").reasoning_options).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, - ]); - expect(build("claude-opus-5").reasoning_options).toEqual([ - { type: "effort", values: ["low", "medium", "high", "xhigh"] }, - ]); - // Native upstream shapes never reach a caller: the host normalizes everything onto - // reasoning_effort, so these are effort lists, not thinking budgets. - expect(build("claude-fable-5").reasoning_options).toEqual([ - { type: "effort", values: ["low", "medium", "high", "xhigh"] }, - ]); - expect(build("kimi-k2-thinking").reasoning_options).toEqual([ - { type: "effort", values: ["high"] }, - ]); - expect(build("gpt-5.4").reasoning_options).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, - ]); - // Only models whose lab and peers document no graded level stay a plain on/off. - expect(build("glm-4.6v-flash").reasoning_options).toEqual([{ type: "toggle" }]); - - for (const id of ["gpt-5.6-terra", "claude-opus-5", "glm-5.3-flash", "laguna-s-2.1"]) { - const options = build(id).reasoning_options ?? []; + const ids = [ + ["gpt-5.6-terra", "openai/gpt-5.6-terra"], + ["claude-sonnet-5", "anthropic/claude-sonnet-5"], + ["deepseek-v4-pro", "deepseek/deepseek-v4-pro"], + ["kimi-k2.5", "moonshotai/kimi-k2.5"], + ["glm-5.3-flash", "zhipuai/glm-5.3-flash"], + ] as const; + + for (const [id, base] of ids) { + const options = neosantaraReasoningControls(id, base); + // A thinking budget and a bare toggle both describe fields this host does not expose. expect(options.some((option) => option.type === "budget_tokens")).toBe(false); - const values = options.flatMap((option) => ("values" in option ? option.values : [])); - expect(values).not.toContain("max"); + expect(options.some((option) => option.type === "toggle")).toBe(false); + for (const value of options.flatMap((o) => ("values" in o ? o.values : []))) { + expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); + } } + + // The request enum is never dumped wholesale onto a model. + const built = buildNeosantaraModel({ ...source, id: "gpt-5.6-terra" }, undefined); + expect(built.reasoning_options).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); }); test("syncs a reasoning model with no per-model configuration in the module", () => { @@ -458,29 +454,37 @@ test("skips a model with unusable upstream pricing instead of aborting the whole }); test("derives reasoning controls from the canonical tree so new models need no code change", () => { - // Nothing about these ids is listed in the module; the values come from the lab entry - // and its same-surface peers, intersected with what this host accepts. + // Lab entry wins, intersected with what this host accepts, so `max` never survives. expect(neosantaraReasoningControls("gpt-5.6-terra", "openai/gpt-5.6-terra")).toEqual([ { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, ]); expect(neosantaraReasoningControls("claude-fable-5", "anthropic/claude-fable-5")).toEqual([ { type: "effort", values: ["low", "medium", "high", "xhigh"] }, ]); - expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) - .toEqual([{ type: "effort", values: ["high"] }]); - expect(neosantaraReasoningControls("gemini-3.7-flash", "google/gemini-3.7-flash")).toEqual([ - { type: "effort", values: ["low", "medium", "high"] }, - ]); - // No lab or peer entry documents a graded level for this one. - expect(neosantaraReasoningControls("glm-4.6v-flash", "zhipuai/glm-4.6v-flash")).toEqual([ - { type: "toggle" }, + // A lab toggle is this host's `reasoning_effort = none`, so it joins the effort list. + expect(neosantaraReasoningControls("deepseek-v4-pro", "deepseek/deepseek-v4-pro")).toEqual([ + { type: "effort", values: ["none", "high"] }, ]); - // A model nobody has authored yet still resolves to something usable. - expect(neosantaraReasoningControls("brand-new", "openai/gpt-5.6-sol")).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + // An always-on reasoner keeps an empty control set rather than borrowing a peer's levels. + expect(neosantaraReasoningControls("kimi-k2-thinking", "moonshotai/kimi-k2-thinking")).toEqual([]); + + // Toggle-only labs expose just the off switch on this host, never invented levels. + expect(neosantaraReasoningControls("kimi-k2.5", "moonshotai/kimi-k2.5")).toEqual([ + { type: "effort", values: ["none"] }, + ]); + expect(neosantaraReasoningControls("laguna-s-2.1", "poolside/laguna-s-2.1")).toEqual([ + { type: "effort", values: ["none"] }, ]); + + // Nothing authors a bare toggle, which would claim a separate on/off field. + for (const [id, base] of [ + ["glm-4.6v-flash", "zhipuai/glm-4.6v-flash"], + ["ling-3.0-flash-fin", "inclusionai/ling-3.0-flash-fin"], + ] as const) { + expect(neosantaraReasoningControls(id, base).some((o) => o.type === "toggle")).toBe(false); + } }); test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index 7171723b679..d54e722be45 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -3,7 +3,7 @@ structured_output = true [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index d0815ef86b0..f1ac707233d 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -4,7 +4,7 @@ structured_output = false [[reasoning_options]] type = "effort" -values = ["low", "high"] +values = ["none", "low", "high"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 6316316d068..9341b8e2351 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -2,7 +2,7 @@ base_model = "deepseek/deepseek-v4-pro-0813" [[reasoning_options]] type = "effort" -values = ["high"] +values = ["none", "low", "high"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 26df780c415..16dab881e96 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -4,7 +4,7 @@ structured_output = false [[reasoning_options]] type = "effort" -values = ["high"] +values = ["none", "high"] [cost] input = 2.4 diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 81b01d4c25c..277e9a37e12 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,9 +1,9 @@ -# Toggle: reasoning_effort = none turns reasoning off base_model = "zhipuai/glm-4.6v-flash" structured_output = false [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none"] [cost] input = 0.015625 diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index 0dd5f96985f..edc30b95a80 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,9 +1,6 @@ base_model = "moonshotai/kimi-k2-thinking" structured_output = false - -[[reasoning_options]] -type = "effort" -values = ["high"] +reasoning_options = [] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 4a120003a3f..8c081e4278e 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -3,7 +3,7 @@ structured_output = false [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none"] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 6e7ad67ae60..98bd741f576 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -3,7 +3,7 @@ attachment = false [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none"] [cost] input = 0.285 diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 9484f9c3027..f1f1ff92899 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -3,7 +3,7 @@ attachment = false [[reasoning_options]] type = "effort" -values = ["low", "high"] +values = ["none", "low", "high"] [cost] input = 3 diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index bf64c0cc244..e80aa583402 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -2,7 +2,7 @@ base_model = "poolside/laguna-s-2.1" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none"] [cost] input = 0 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index a4ab563b064..ca89b0570db 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,8 +1,8 @@ -# Toggle: reasoning_effort = none turns reasoning off base_model = "poolside/laguna-xs-2.1" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none"] [cost] input = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 97c2e1cb873..a8daeb41868 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,9 +1,9 @@ -# Toggle: reasoning_effort = none turns reasoning off base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none"] [cost] input = 0 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index a40fcb7b232..aac9c67fa93 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,9 +1,6 @@ base_model = "minimax/MiniMax-M2.7" structured_output = true - -[[reasoning_options]] -type = "effort" -values = ["low", "medium", "high", "xhigh"] +reasoning_options = [] [cost] input = 0.09 From a077b00b53dd1b34801cba4f9b8dbfb679b175dc Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 08:08:29 +0000 Subject: [PATCH 08/29] docs(neosantara): record the host's reasoning and pricing wire format Reading the gateway's public schemas end to end corrected one thing and explained several others. No model may author a toggle here. `reasoning.enabled` on its own is inert: reasoning is switched on by `reasoning_effort` being anything other than `none`, so a toggle would name a control a caller cannot use. Models whose lab documents only a binary on/off therefore author the one effort value that changes behaviour. provider.toml now carries the wire format the way the established relays do: the accepted effort values, how reasoning is switched off, that a non-reasoning model rejects the field outright, and that published costs are the raw price after the catalog discount with IDR converted at the rate the pricing endpoint reports. --- .../core/src/sync/providers/neosantara.ts | 24 +++++++++++++++---- packages/core/test/neosantara-sync.test.ts | 15 +++++------- .../models/deepseek-v4-pro-0813.toml | 2 +- providers/neosantara/provider.toml | 14 +++++++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 61ee9f28c4d..ccce89aa78f 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -154,6 +154,13 @@ const HOST_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh /** A model that reasons with no caller control. */ const ALWAYS_ON = "always-on"; +/** + * A model whose only caller choice is whether to reason at all. This host has no separate + * on/off field a caller can use: `reasoning.enabled` alone is inert, because reasoning is + * switched on by `reasoning_effort` being anything other than `none`. So the control is an + * effort list holding the one value that changes behaviour. + */ +const ON_OFF_ONLY = "on-off"; function tomlFilesIn(dir: string): string[] { let entries; @@ -186,7 +193,9 @@ function parseToml(filePath: string) { * joins the effort list rather than staying a separate option — a bare toggle would claim an * on/off field this host does not have. Values the host cannot send are dropped. */ -function hostControls(options: unknown): string[] | typeof ALWAYS_ON | undefined { +function hostControls( + options: unknown, +): string[] | typeof ALWAYS_ON | typeof ON_OFF_ONLY | undefined { if (!Array.isArray(options)) return undefined; if (options.length === 0) return ALWAYS_ON; @@ -205,7 +214,7 @@ function hostControls(options: unknown): string[] | typeof ALWAYS_ON | undefined if (usable.length > 0) accepted = usable; } - if (accepted === undefined) return toggled ? ["none"] : undefined; + if (accepted === undefined) return toggled ? ON_OFF_ONLY : undefined; return toggled && !accepted.includes("none") ? ["none", ...accepted] : accepted; } @@ -232,7 +241,7 @@ function indexControls() { const base = toml.base_model; if (typeof base !== "string" || controls === undefined) continue; - const key = controls === ALWAYS_ON ? ALWAYS_ON : controls.join(","); + const key = typeof controls === "string" ? controls : controls.join(","); const counts = tally.get(base) ?? new Map(); counts.set(key, (counts.get(key) ?? 0) + 1); tally.set(base, counts); @@ -262,13 +271,18 @@ function indexControls() { export function neosantaraReasoningControls(id: string, baseModel: string): ReasoningControls { const { lab, peers } = indexControls(); const [owner, ...rest] = baseModel.split("/"); - const key = `${owner}/${rest.at(-1)}`; - const derived = lab.has(key) ? lab.get(key) : peers.get(baseModel); + const name = rest.at(-1) ?? ""; + // A dated snapshot such as `-0813` shares its lab entry with the undated model. + const candidates = [name, name.replace(/-\d{4,}$/, "")]; + const key = candidates.map((candidate) => `${owner}/${candidate}`).find((k) => lab.has(k)); + const derived = key === undefined ? peers.get(baseModel) : lab.get(key); if (derived === undefined || derived === ALWAYS_ON) return []; + if (derived === ON_OFF_ONLY) return [{ type: "effort", values: ["none"] as never }]; return [{ type: "effort", values: derived as never }]; } + /** Image models are priced per image and carry no context window, so they skip the token filters. */ function isImageModel(model: NeosantaraSourceModel) { return model.capabilities.includes("image_generation"); diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 8540529fb72..87240ee6970 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -166,7 +166,7 @@ test("never advertises a control this host cannot send", () => { for (const [id, base] of ids) { const options = neosantaraReasoningControls(id, base); - // A thinking budget and a bare toggle both describe fields this host does not expose. + // Neither a thinking budget nor a bare toggle names a field a caller can use here. expect(options.some((option) => option.type === "budget_tokens")).toBe(false); expect(options.some((option) => option.type === "toggle")).toBe(false); for (const value of options.flatMap((o) => ("values" in o ? o.values : []))) { @@ -470,7 +470,8 @@ test("derives reasoning controls from the canonical tree so new models need no c // An always-on reasoner keeps an empty control set rather than borrowing a peer's levels. expect(neosantaraReasoningControls("kimi-k2-thinking", "moonshotai/kimi-k2-thinking")).toEqual([]); - // Toggle-only labs expose just the off switch on this host, never invented levels. + // A lab with no graded level leaves one meaningful value: off. This host has no separate + // on/off field, so a toggle would name a control a caller cannot actually use. expect(neosantaraReasoningControls("kimi-k2.5", "moonshotai/kimi-k2.5")).toEqual([ { type: "effort", values: ["none"] }, ]); @@ -478,13 +479,9 @@ test("derives reasoning controls from the canonical tree so new models need no c { type: "effort", values: ["none"] }, ]); - // Nothing authors a bare toggle, which would claim a separate on/off field. - for (const [id, base] of [ - ["glm-4.6v-flash", "zhipuai/glm-4.6v-flash"], - ["ling-3.0-flash-fin", "inclusionai/ling-3.0-flash-fin"], - ] as const) { - expect(neosantaraReasoningControls(id, base).some((o) => o.type === "toggle")).toBe(false); - } + // A dated snapshot resolves to its lab entry rather than falling through to peers. + expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) + .toEqual([{ type: "effort", values: ["none", "high"] }]); }); test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 9341b8e2351..963a18be008 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -2,7 +2,7 @@ base_model = "deepseek/deepseek-v4-pro-0813" [[reasoning_options]] type = "effort" -values = ["none", "low", "high"] +values = ["none", "high"] [cost] input = 1.32 diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index 713d0729255..00894677fbd 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -1,3 +1,17 @@ +# Sources (accessed 2026-09-03): +# - https://docs.neosantara.xyz +# - https://api.neosantara.xyz/v1/models (catalog, public, no key) +# - https://api.neosantara.xyz/v1/public/pricing (raw_pricing + meta.exchange_rate, public) +# +# Reasoning wire format on POST /v1/chat/completions: +# Effort: reasoning_effort = none|minimal|low|medium|high|xhigh +# Off is reasoning_effort = none; anything else turns reasoning on. +# There is no separate caller-usable on/off field, so no model here authors a toggle. +# A model without the `reasoning` capability rejects the field with HTTP 400. +# +# Pricing: raw_pricing is per million tokens in USD or IDR and excludes `discount`, +# which is a percentage applied at billing time. Published costs are therefore +# raw * (100 - discount) / 100, with IDR converted at meta.exchange_rate.usd_idr. name = "Neosantara" env = ["NEOSANTARA_API_KEY"] npm = "@ai-sdk/openai-compatible" From c908cfddbb79476839003d4a5dd4504ac44c7ddd Mon Sep 17 00:00:00 2001 From: Just R Date: Thu, 3 Sep 2026 15:31:10 +0700 Subject: [PATCH 09/29] Update logo as pure Vectors --- providers/neosantara/logo.svg | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index 3767200fad3..b66996874e6 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -1,3 +1,11 @@ - - + + + + + + + + + + From 66f733c854c13f9f961f11de31d99b69164cb774 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 08:48:04 +0000 Subject: [PATCH 10/29] fix(neosantara): author binary reasoning as a toggle with its wire comment Six models whose lab documents only an on/off were published as effort ["none"], which told a caller how to switch reasoning off but named no value that switches it on. They are toggles now, each carrying a leading comment for the mapping this host uses: reasoning_effort = "none" is off, any other accepted value leaves it on, and reasoning.enabled alone does nothing. Follows the same shape and header convention as the LLM Gateway sync, which fronts many labs behind one reasoning_effort field too. --- .../core/src/sync/providers/neosantara.ts | 25 +++++++++++++++---- packages/core/test/neosantara-sync.test.ts | 22 +++++++++------- .../neosantara/models/glm-4.6v-flash.toml | 7 ++++-- providers/neosantara/models/kimi-k2.5.toml | 7 ++++-- providers/neosantara/models/kimi-k2.6.toml | 7 ++++-- providers/neosantara/models/laguna-s-2.1.toml | 7 ++++-- .../neosantara/models/laguna-xs-2.1.toml | 7 ++++-- .../neosantara/models/ling-3.0-flash-fin.toml | 7 ++++-- 8 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index ccce89aa78f..c6cfd6ac3e1 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -155,13 +155,18 @@ const HOST_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh /** A model that reasons with no caller control. */ const ALWAYS_ON = "always-on"; /** - * A model whose only caller choice is whether to reason at all. This host has no separate - * on/off field a caller can use: `reasoning.enabled` alone is inert, because reasoning is - * switched on by `reasoning_effort` being anything other than `none`. So the control is an - * effort list holding the one value that changes behaviour. + * A model whose only caller choice is whether to reason at all. The switch is + * `reasoning_effort`, not a separate field — `reasoning.enabled` on its own is inert — so the + * control is a toggle and the header below records the mapping. */ const ON_OFF_ONLY = "on-off"; +const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz +`; + function tomlFilesIn(dir: string): string[] { let entries; try { @@ -268,6 +273,11 @@ function indexControls() { * the canonical tree at sync time instead of being listed here: the model's own lab entry * wins, otherwise the shape its peers agree on. New models need no change here. */ +/** A toggle control must carry a leading comment naming the field it refers to. */ +export function neosantaraReasoningHeader(controls: ReasoningControls) { + return controls.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; +} + export function neosantaraReasoningControls(id: string, baseModel: string): ReasoningControls { const { lab, peers } = indexControls(); const [owner, ...rest] = baseModel.split("/"); @@ -278,7 +288,7 @@ export function neosantaraReasoningControls(id: string, baseModel: string): Reas const derived = key === undefined ? peers.get(baseModel) : lab.get(key); if (derived === undefined || derived === ALWAYS_ON) return []; - if (derived === ON_OFF_ONLY) return [{ type: "effort", values: ["none"] as never }]; + if (derived === ON_OFF_ONLY) return [{ type: "toggle" }]; return [{ type: "effort", values: derived as never }]; } @@ -433,6 +443,11 @@ export const neosantara = { return { id: model.id, model: buildNeosantaraModel(model, context.existing(model.id)), + header: model.capabilities.includes("reasoning") + ? neosantaraReasoningHeader( + neosantaraReasoningControls(model.id, resolveNeosantaraBaseModel(model.id) ?? ""), + ) + : undefined, }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 87240ee6970..ebabc5838d1 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -6,6 +6,7 @@ import { neosantara, neosantaraInputModalities, neosantaraReasoningControls, + neosantaraReasoningHeader, NeosantaraModelsResponse, NeosantaraPricingResponse, resolveNeosantaraBaseModel, @@ -160,8 +161,8 @@ test("never advertises a control this host cannot send", () => { ["gpt-5.6-terra", "openai/gpt-5.6-terra"], ["claude-sonnet-5", "anthropic/claude-sonnet-5"], ["deepseek-v4-pro", "deepseek/deepseek-v4-pro"], - ["kimi-k2.5", "moonshotai/kimi-k2.5"], ["glm-5.3-flash", "zhipuai/glm-5.3-flash"], + ["gemini-3.7-flash", "google/gemini-3.7-flash"], ] as const; for (const [id, base] of ids) { @@ -169,6 +170,7 @@ test("never advertises a control this host cannot send", () => { // Neither a thinking budget nor a bare toggle names a field a caller can use here. expect(options.some((option) => option.type === "budget_tokens")).toBe(false); expect(options.some((option) => option.type === "toggle")).toBe(false); + expect(neosantaraReasoningHeader(options)).toBeUndefined(); for (const value of options.flatMap((o) => ("values" in o ? o.values : []))) { expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); } @@ -470,14 +472,16 @@ test("derives reasoning controls from the canonical tree so new models need no c // An always-on reasoner keeps an empty control set rather than borrowing a peer's levels. expect(neosantaraReasoningControls("kimi-k2-thinking", "moonshotai/kimi-k2-thinking")).toEqual([]); - // A lab with no graded level leaves one meaningful value: off. This host has no separate - // on/off field, so a toggle would name a control a caller cannot actually use. - expect(neosantaraReasoningControls("kimi-k2.5", "moonshotai/kimi-k2.5")).toEqual([ - { type: "effort", values: ["none"] }, - ]); - expect(neosantaraReasoningControls("laguna-s-2.1", "poolside/laguna-s-2.1")).toEqual([ - { type: "effort", values: ["none"] }, - ]); + // A lab with no graded level is a binary control, carried with a wire comment rather than + // an effort list that could only say how to switch reasoning off. + for (const [id, base] of [ + ["kimi-k2.5", "moonshotai/kimi-k2.5"], + ["laguna-s-2.1", "poolside/laguna-s-2.1"], + ] as const) { + const controls = neosantaraReasoningControls(id, base); + expect(controls).toEqual([{ type: "toggle" }]); + expect(neosantaraReasoningHeader(controls)).toContain("reasoning_effort"); + } // A dated snapshot resolves to its lab entry rather than falling through to peers. expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 277e9a37e12..31b3339449e 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,9 +1,12 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "zhipuai/glm-4.6v-flash" structured_output = false [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0.015625 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 8c081e4278e..3b446997edc 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,9 +1,12 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.5" structured_output = false [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 98bd741f576..31652a5340d 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,9 +1,12 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.6" attachment = false [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0.285 diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index e80aa583402..9d440a83171 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,8 +1,11 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index ca89b0570db..5331ddd1a38 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,8 +1,11 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index a8daeb41868..f9cf7c5b713 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,9 +1,12 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value (or omitting the field) leaves it on. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false [[reasoning_options]] -type = "effort" -values = ["none"] +type = "toggle" [cost] input = 0 From ae8062f1dac9238e41d7d31843e6287a82dba04e Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 08:49:30 +0000 Subject: [PATCH 11/29] fix(neosantara): let the logo inherit theme color The vector mark still carried six hex fills and strokes, so it rendered the same shade on both themes. Every fill and stroke is currentColor now, which is what the site inlines against, while the existing opacity values keep the depth of the original artwork. --- providers/neosantara/logo.svg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index b66996874e6..73b827eac55 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -1,11 +1,11 @@ - - - - - - - - + + + + + + + + From d469ff879b949a06473f0a6f7370e3af53534fb8 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 10:54:31 +0000 Subject: [PATCH 12/29] fix(neosantara): trust the gateway catalog for capability flags Capability flags are read only from each catalog entry's `capabilities` array, never inherited from lab metadata: this gateway enforces its own list and answers HTTP 400 for `reasoning_effort` on a model without `reasoning`, so a model the lab ships as a reasoner is published here with reasoning = false until the catalog advertises it. `tool_call` follows `function_calling` for the same reason. A capability the gateway adds later is therefore picked up by the next sync with no code change. Reasoning models gain `interleaved.field = "reasoning_content"`. Verified against the live API through the AI SDK with streaming: `reasoning_effort = none` returns zero reasoning tokens, any graded level streams `reasoning_content` deltas, and `max` is rejected with the enum `none|minimal|low|medium|high|xhigh`. Reasoning-control indexing: the provider tree is resolved from the module rather than the working directory, an unreadable tree throws instead of silently publishing "no caller control" for every reasoning model, and a lab entry whose controls this host cannot express is left unrecorded so peer consensus still applies. Model TOMLs regenerated with `sync-models.ts neosantara`; re-running it reports no further changes. Also lets the remaining logo paths inherit the theme color. --- .../core/src/sync/providers/neosantara.ts | 40 ++++++++++++++----- packages/core/test/neosantara-sync.test.ts | 39 ++++++++++++++++++ providers/neosantara/logo.svg | 6 +-- .../neosantara/models/claude-fable-5.toml | 3 ++ .../neosantara/models/claude-opus-4-6.toml | 3 ++ .../neosantara/models/claude-opus-5.toml | 3 ++ .../neosantara/models/claude-opus-7.toml | 3 ++ .../neosantara/models/claude-opus-8.toml | 3 ++ .../neosantara/models/claude-sonnet-4-6.toml | 3 ++ .../neosantara/models/claude-sonnet-5.toml | 3 ++ .../neosantara/models/deepseek-v4-flash.toml | 3 ++ .../models/deepseek-v4-pro-0813.toml | 3 ++ .../neosantara/models/deepseek-v4-pro.toml | 3 ++ .../neosantara/models/gemini-3.6-flash.toml | 3 ++ .../neosantara/models/gemini-3.7-flash.toml | 3 ++ .../neosantara/models/glm-4.6v-flash.toml | 3 ++ .../neosantara/models/glm-5.3-flash.toml | 3 ++ providers/neosantara/models/gpt-5-nano.toml | 3 ++ providers/neosantara/models/gpt-5.4-nano.toml | 3 ++ providers/neosantara/models/gpt-5.4.toml | 3 ++ providers/neosantara/models/gpt-5.5.toml | 3 ++ providers/neosantara/models/gpt-5.6-luna.toml | 3 ++ providers/neosantara/models/gpt-5.6-sol.toml | 3 ++ .../neosantara/models/gpt-5.6-terra.toml | 3 ++ .../neosantara/models/kimi-k2-thinking.toml | 3 ++ providers/neosantara/models/kimi-k2.5.toml | 3 ++ providers/neosantara/models/kimi-k2.6.toml | 3 ++ providers/neosantara/models/kimi-k3.toml | 3 ++ providers/neosantara/models/laguna-s-2.1.toml | 3 ++ .../neosantara/models/laguna-xs-2.1.toml | 3 ++ .../neosantara/models/ling-3.0-flash-fin.toml | 3 ++ providers/neosantara/models/minimax-m2.7.toml | 3 ++ .../neosantara/models/muse-spark-1.1.toml | 3 ++ .../neosantara/models/step-3.5-flash.toml | 3 ++ providers/neosantara/provider.toml | 8 +++- 35 files changed, 173 insertions(+), 13 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index c6cfd6ac3e1..3eed857d34e 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -8,7 +8,9 @@ import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js" const MODELS_ENDPOINT = "https://api.neosantara.xyz/v1/models"; const PRICING_ENDPOINT = "https://api.neosantara.xyz/v1/public/pricing"; const MIN_CONTEXT_WINDOW = 100_000; -const PROVIDERS_DIR = path.join(process.cwd(), "providers"); +// Resolved from this module, not the working directory: the tree is what every reasoning +// control is derived from, so it must not depend on where the sync happens to be launched. +const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers"); const Pricing = z.object({ currency: z.enum(["USD", "IDR"]), @@ -232,17 +234,22 @@ function indexControls() { const lab = new Map>(); const tally = new Map>(); const shapes = new Map>>(); + let parsed = 0; for (const file of tomlFilesIn(PROVIDERS_DIR)) { if (file.startsWith(path.join(PROVIDERS_DIR, "neosantara"))) continue; const toml = parseToml(file); if (toml === undefined) continue; + parsed++; const controls = hostControls(toml.reasoning_options); - // A lab's own directory is authoritative, wherever the file sits inside it. + // A lab's own directory is authoritative, wherever the file sits inside it. An entry whose + // controls cannot be read is left unrecorded so peer consensus still gets a say; recording + // it as `undefined` would claim the lab documents no control and settle for always-on. const owner = path.relative(PROVIDERS_DIR, file).split(path.sep)[0]; const name = path.basename(file, ".toml"); - if (owner !== undefined && !lab.has(`${owner}/${name}`)) lab.set(`${owner}/${name}`, controls); + const labKey = `${owner}/${name}`; + if (owner !== undefined && controls !== undefined && !lab.has(labKey)) lab.set(labKey, controls); const base = toml.base_model; if (typeof base !== "string" || controls === undefined) continue; @@ -255,6 +262,15 @@ function indexControls() { shapes.set(base, seen); } + // Every control on this host is derived from the tree, so an unreadable tree is not an empty + // answer — it would publish "no caller control" for every reasoning model. Fail loudly: + // the causes are an incomplete checkout or a runtime whose `Bun.TOML` cannot parse the files. + if (parsed === 0) { + throw new Error( + `Neosantara reasoning controls need the provider tree, but no TOML under '${PROVIDERS_DIR}' could be read. Run the sync from a full checkout with Bun.`, + ); + } + const peers = new Map>(); for (const [base, counts] of tally) { const [best] = [...counts.entries()].sort((a, b) => b[1] - a[1]); @@ -362,6 +378,11 @@ export function buildNeosantaraModel( ); } + // Capabilities come from this host's catalog, never from the lab entry: the gateway enforces + // its own list and answers HTTP 400 when a request uses a capability the model does not + // advertise here (`reasoning_effort` on a model without `reasoning`), no matter what the + // underlying model can do elsewhere. Reading them live means a capability the host adds later + // is picked up by the next sync with no code change. const reasoning = model.capabilities.includes("reasoning"); const inputCost = effectiveUsd(model.pricing.prompt, model); const outputCost = effectiveUsd(model.pricing.completion, model); @@ -381,8 +402,10 @@ export function buildNeosantaraModel( name: NAME_OVERRIDES[model.id], reasoning, reasoning_options: reasoning ? neosantaraReasoningControls(model.id, baseModel) : undefined, + // Reasoning is streamed back in `reasoning_content`, whichever lab built the model. + interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: model.capabilities.includes("vision"), - tool_call: true, + tool_call: model.capabilities.includes("function_calling"), structured_output: model.capabilities.includes("json_mode"), modalities: { input: neosantaraInputModalities(model.capabilities), @@ -440,13 +463,12 @@ export const neosantara = { }, translateModel(model, context) { if (!shouldSyncNeosantaraModel(model)) return undefined; + const translated = buildNeosantaraModel(model, context.existing(model.id)); return { id: model.id, - model: buildNeosantaraModel(model, context.existing(model.id)), - header: model.capabilities.includes("reasoning") - ? neosantaraReasoningHeader( - neosantaraReasoningControls(model.id, resolveNeosantaraBaseModel(model.id) ?? ""), - ) + model: translated, + header: translated.reasoning_options !== undefined + ? neosantaraReasoningHeader(translated.reasoning_options) : undefined, }; }, diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index ebabc5838d1..00973d343ae 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -1,4 +1,6 @@ import { expect, test } from "bun:test"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { buildNeosantaraModel, @@ -139,16 +141,23 @@ test("builds override-only models with effective USD pricing and host reasoning expect(gemini).toMatchObject({ base_model: "google/gemini-3.7-flash", reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }], + // This host streams reasoning in `reasoning_content`. + interleaved: { field: "reasoning_content" }, limit: { context: 1_000_000 }, cost: { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 }, }); + // The host's own capability list decides whether reasoning is served here: it rejects + // `reasoning_effort` with HTTP 400 for a model that does not advertise `reasoning`, however + // the lab entry describes the underlying model. const idr = buildNeosantaraModel(merged[1]!, undefined); expect(idr).toMatchObject({ base_model: "openai/gpt-oss-20b", reasoning: false, cost: { input: 0.02, output: 0.08 }, }); + expect(idr.reasoning_options).toBeUndefined(); + expect(idr.interleaved).toBeUndefined(); }); test("never advertises a control this host cannot send", () => { @@ -488,6 +497,36 @@ test("derives reasoning controls from the canonical tree so new models need no c .toEqual([{ type: "effort", values: ["none", "high"] }]); }); +test("falls through to peers when the lab documents only a control this host cannot send", () => { + // Anthropic's own entry for this model is a thinking budget, which no caller of this host can + // set. That is unknown, not always-on, so peer consensus decides instead of publishing `[]`. + const controls = neosantaraReasoningControls("claude-4.5-sonnet", "anthropic/claude-sonnet-4-5"); + + expect(controls.length).toBeGreaterThan(0); + expect(controls.some((option) => option.type === "budget_tokens")).toBe(false); + for (const value of controls.flatMap((option) => ("values" in option ? option.values : []))) { + expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); + } +}); + +test("reads the provider tree from the module, not the working directory", () => { + const module = path.join(import.meta.dir, "..", "src", "sync", "providers", "neosantara.ts"); + const run = Bun.spawnSync( + [ + "bun", + "-e", + `const { neosantaraReasoningControls } = await import(${JSON.stringify(module)}); + console.log(JSON.stringify(neosantaraReasoningControls("gpt-5.4", "openai/gpt-5.4")));`, + ], + { cwd: tmpdir() }, + ); + + expect(run.stderr.toString()).toBe(""); + expect(JSON.parse(run.stdout.toString())).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); +}); + test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { const model = { ...mergeNeosantaraCatalogs( diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index 73b827eac55..875edca49bb 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -5,7 +5,7 @@ - - - + + + diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 32ead780e21..5b43c0b5381 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -2,6 +2,9 @@ base_model = "anthropic/claude-fable-5" attachment = false structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 9041faaa550..2ee956d624e 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -2,6 +2,9 @@ base_model = "anthropic/claude-opus-4-6" attachment = false structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 30d6eb25cae..52917a6e0da 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -1,6 +1,9 @@ base_model = "anthropic/claude-opus-5" structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/claude-opus-7.toml b/providers/neosantara/models/claude-opus-7.toml index c72a6f60cb2..3bd4e68f667 100644 --- a/providers/neosantara/models/claude-opus-7.toml +++ b/providers/neosantara/models/claude-opus-7.toml @@ -1,6 +1,9 @@ base_model = "anthropic/claude-opus-4-7" structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/claude-opus-8.toml b/providers/neosantara/models/claude-opus-8.toml index e6bc340d309..c5fe963c3ed 100644 --- a/providers/neosantara/models/claude-opus-8.toml +++ b/providers/neosantara/models/claude-opus-8.toml @@ -1,6 +1,9 @@ base_model = "anthropic/claude-opus-4-8" structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index a92d02f8b48..1b84030982c 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -2,6 +2,9 @@ base_model = "anthropic/claude-sonnet-4-6" attachment = false structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index d54e722be45..4478b6ab406 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,6 +1,9 @@ base_model = "anthropic/claude-sonnet-5" structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index f1ac707233d..db116c8d3c3 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -2,6 +2,9 @@ base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "high"] diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 963a18be008..98bea81ecc8 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,5 +1,8 @@ base_model = "deepseek/deepseek-v4-pro-0813" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "high"] diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 16dab881e96..91515a4140a 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -2,6 +2,9 @@ base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "high"] diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 85e08c1c2c3..4065d09c643 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -1,5 +1,8 @@ base_model = "google/gemini-3.6-flash" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["minimal", "low", "medium", "high"] diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 5e0a2dadbec..46aa16386a5 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -1,5 +1,8 @@ base_model = "google/gemini-3.7-flash" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 31b3339449e..6c97984a139 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -5,6 +5,9 @@ base_model = "zhipuai/glm-4.6v-flash" structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml index 876bdd3ba77..b1e7437b7ac 100644 --- a/providers/neosantara/models/glm-5.3-flash.toml +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -1,6 +1,9 @@ base_model = "zhipuai/glm-5.3-flash" attachment = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "high"] diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index 01b8623f3d0..1d8b65e0bc4 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -1,6 +1,9 @@ base_model = "openai/gpt-5-nano" base_model_omit = ["limit.input"] +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["minimal", "low", "medium", "high"] diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index 1fb8800ae75..0c7f54843de 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -1,5 +1,8 @@ base_model = "openai/gpt-5.4-nano" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 20eaba88c87..67d20995843 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -2,6 +2,9 @@ base_model = "openai/gpt-5.4" base_model_omit = ["limit.input"] attachment = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index b2faccff55a..3b86415307d 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -2,6 +2,9 @@ base_model = "openai/gpt-5.5" base_model_omit = ["limit.input"] attachment = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml index 863a3844bbc..fd02a7e98dc 100644 --- a/providers/neosantara/models/gpt-5.6-luna.toml +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -3,6 +3,9 @@ base_model_omit = ["limit.input"] attachment = false structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml index 0913c0ec57d..0f493c7de28 100644 --- a/providers/neosantara/models/gpt-5.6-sol.toml +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -3,6 +3,9 @@ base_model_omit = ["limit.input"] attachment = false structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index 832ad6c65b6..e485bb96b90 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -2,6 +2,9 @@ base_model = "openai/gpt-5.6-terra" base_model_omit = ["limit.input"] structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index edc30b95a80..62d92c7bac9 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -2,6 +2,9 @@ base_model = "moonshotai/kimi-k2-thinking" structured_output = false reasoning_options = [] +[interleaved] +field = "reasoning_content" + [cost] input = 0.6 output = 2.5 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 3b446997edc..714b06b332d 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -5,6 +5,9 @@ base_model = "moonshotai/kimi-k2.5" structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 31652a5340d..4d64e49ae70 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -5,6 +5,9 @@ base_model = "moonshotai/kimi-k2.6" attachment = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index f1f1ff92899..86cc82fbb35 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,6 +1,9 @@ base_model = "moonshotai/kimi-k3" attachment = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["none", "low", "high"] diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index 9d440a83171..00037b1bdb5 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -4,6 +4,9 @@ # https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index 5331ddd1a38..e66616b98bc 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -4,6 +4,9 @@ # https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index f9cf7c5b713..08009ad97c3 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -5,6 +5,9 @@ base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "toggle" diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index aac9c67fa93..47f5f97e08f 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -2,6 +2,9 @@ base_model = "minimax/MiniMax-M2.7" structured_output = true reasoning_options = [] +[interleaved] +field = "reasoning_content" + [cost] input = 0.09 output = 0.36 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index 98d05e03b0d..c81c2320be0 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -1,6 +1,9 @@ base_model = "meta/muse-spark-1.1" structured_output = false +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["minimal", "low", "medium", "high", "xhigh"] diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 8891addb422..2c5cfb3a8bf 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,6 +1,9 @@ base_model = "stepfun/step-3.5-flash" structured_output = true +[interleaved] +field = "reasoning_content" + [[reasoning_options]] type = "effort" values = ["low", "high"] diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index 00894677fbd..d50f9212efa 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -6,9 +6,15 @@ # Reasoning wire format on POST /v1/chat/completions: # Effort: reasoning_effort = none|minimal|low|medium|high|xhigh # Off is reasoning_effort = none; anything else turns reasoning on. -# There is no separate caller-usable on/off field, so no model here authors a toggle. +# Binary on/off controls are authored as `toggle`, where reasoning_effort = "none" +# disables reasoning and any other accepted value (or omitting the field) leaves it on. # A model without the `reasoning` capability rejects the field with HTTP 400. # +# Capability flags (reasoning, vision, json_mode, ...) are read from each catalog entry's +# `capabilities` array and never inherited from lab metadata, because this gateway enforces +# its own list: a model the lab ships as a reasoner is published here with reasoning = false +# until the catalog advertises it, and starts publishing controls on the sync after it does. +# # Pricing: raw_pricing is per million tokens in USD or IDR and excludes `discount`, # which is a percentage applied at billing time. Published costs are therefore # raw * (100 - discount) / 100, with IDR converted at meta.exchange_rate.usd_idr. From 29cca6c26d6e8f75b3370756a99e5480b8ed066f Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 11:22:42 +0000 Subject: [PATCH 13/29] fix(neosantara): preserve reasoning control certainty Treat missing lab and peer reasoning controls as unresolved instead of publishing an affirmative always-on control set. Such models are now skipped and reported until a reviewed control mapping exists; an empty list remains reserved for explicitly always-on models. Neosantara's DeepSeek route only consumes effort as an enable flag and does not forward graded effort upstream, so publish those routes as a binary toggle rather than claiming high/max distinctions the gateway cannot honor. Correct the toggle header to record that omission defaults to off. Add authoritative FX provenance headers to every model whose IDR source pricing is converted to USD, including the rate, source, and pricing snapshot date. Regenerate provider TOMLs from the public catalog. The gateway capability list remains authoritative for reasoning support: models that omit reasoning from GET /v1/models retain reasoning=false, regardless of provider-agnostic lab metadata. --- .../core/src/sync/providers/neosantara.ts | 65 ++++++++++++++++--- packages/core/test/neosantara-sync.test.ts | 64 ++++++++++++++++-- .../neosantara/models/deepseek-v4-flash.toml | 7 +- .../models/deepseek-v4-pro-0813.toml | 7 +- .../neosantara/models/deepseek-v4-pro.toml | 7 +- .../neosantara/models/glm-4.5-flash.toml | 1 + .../neosantara/models/glm-4.6v-flash.toml | 3 +- providers/neosantara/models/glm-4.7.toml | 1 + providers/neosantara/models/gpt-5-nano.toml | 1 + providers/neosantara/models/gpt-5.4-nano.toml | 1 + providers/neosantara/models/gpt-oss-20b.toml | 1 + providers/neosantara/models/kimi-k2.5.toml | 2 +- providers/neosantara/models/kimi-k2.6.toml | 2 +- providers/neosantara/models/laguna-s-2.1.toml | 2 +- .../neosantara/models/laguna-xs-2.1.toml | 2 +- .../neosantara/models/ling-3.0-flash-fin.toml | 2 +- .../llama-3.3-nemotron-super-49b-v1.5.toml | 1 + .../neosantara/models/step-3.5-flash.toml | 1 + providers/neosantara/provider.toml | 4 +- 19 files changed, 145 insertions(+), 29 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 3eed857d34e..5f14da4c027 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -80,6 +80,8 @@ const NeosantaraCombinedResponse = z.object({ export type NeosantaraSourceModel = z.infer & { exchange_rate: number; + exchange_rate_source: string; + exchange_rate_updated_at: string; }; /** @@ -133,6 +135,8 @@ export function mergeNeosantaraCatalogs( ...model, pricing: publicPricing.get(model.id) ?? model.pricing, exchange_rate: pricing.meta.exchange_rate.usd_idr, + exchange_rate_source: pricing.meta.exchange_rate.source, + exchange_rate_updated_at: pricing.meta.updated_at, })); } @@ -164,7 +168,7 @@ const ALWAYS_ON = "always-on"; const ON_OFF_ONLY = "on-off"; const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz `; @@ -294,16 +298,28 @@ export function neosantaraReasoningHeader(controls: ReasoningControls) { return controls.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; } -export function neosantaraReasoningControls(id: string, baseModel: string): ReasoningControls { - const { lab, peers } = indexControls(); +export function neosantaraReasoningControls( + id: string, + baseModel: string, +): ReasoningControls | undefined { const [owner, ...rest] = baseModel.split("/"); + + // DeepSeek routes are binary on this gateway. The backend consumes any non-`none` effort only + // as `reasoning.enabled = true`; DeepSeekProvider forwards no graded effort upstream. Thus the + // lab's high/max distinction cannot be represented here and must not be advertised. This is a + // family-level request-shape rule, not a capability override: the live catalog still decides + // which individual DeepSeek model IDs reason at all. + if (owner === "deepseek") return [{ type: "toggle" }]; + + const { lab, peers } = indexControls(); const name = rest.at(-1) ?? ""; // A dated snapshot such as `-0813` shares its lab entry with the undated model. const candidates = [name, name.replace(/-\d{4,}$/, "")]; const key = candidates.map((candidate) => `${owner}/${candidate}`).find((k) => lab.has(k)); const derived = key === undefined ? peers.get(baseModel) : lab.get(key); - if (derived === undefined || derived === ALWAYS_ON) return []; + if (derived === undefined) return undefined; + if (derived === ALWAYS_ON) return []; if (derived === ON_OFF_ONLY) return [{ type: "toggle" }]; return [{ type: "effort", values: derived as never }]; } @@ -319,6 +335,26 @@ const NAME_OVERRIDES: Record = { "grok-code-fast": "Grok Code Fast", }; +/** Leading provenance for prices converted from IDR into models.dev's required USD units. */ +export function neosantaraFxHeader(model: NeosantaraSourceModel) { + if (model.pricing.currency !== "IDR") return undefined; + const date = model.exchange_rate_updated_at.slice(0, 10); + const source = model.exchange_rate_source.replace(/[\r\n]+/g, " "); + return `# FX: IDR prices converted to USD at 1 USD = ${model.exchange_rate} IDR` + + ` (${source}, ${date}).\n`; +} + +function neosantaraModelHeader( + model: NeosantaraSourceModel, + controls: ReasoningControls | undefined, +) { + const headers = [ + neosantaraFxHeader(model), + controls === undefined ? undefined : neosantaraReasoningHeader(controls), + ].filter((header): header is string => header !== undefined); + return headers.length === 0 ? undefined : headers.join(""); +} + /** Models this host publishes at all, regardless of whether we can translate them yet. */ export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { return ( @@ -330,11 +366,18 @@ export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { } export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { - if (model.deprecated || resolveNeosantaraBaseModel(model.id) === undefined) return false; + const baseModel = resolveNeosantaraBaseModel(model.id); + if (model.deprecated || baseModel === undefined) return false; if (!meetsNeosantaraPublicFilter(model)) return false; if (isImageModel(model)) return true; // Skip rather than throw: one missing catalog field must cost a single model, not the run. if (model.pricing.prompt === undefined || model.pricing.completion === undefined) return false; + // Empty means explicitly always-on; undefined means neither the lab nor peers document a + // control. Never turn uncertainty into an affirmative no-control claim on a relay. + if ( + model.capabilities.includes("reasoning") + && neosantaraReasoningControls(model.id, baseModel) === undefined + ) return false; return true; } @@ -384,6 +427,12 @@ export function buildNeosantaraModel( // underlying model can do elsewhere. Reading them live means a capability the host adds later // is picked up by the next sync with no code change. const reasoning = model.capabilities.includes("reasoning"); + const reasoningOptions = reasoning + ? neosantaraReasoningControls(model.id, baseModel) + : undefined; + if (reasoning && reasoningOptions === undefined) { + throw new Error(`No reviewed reasoning controls for Neosantara model '${model.id}'`); + } const inputCost = effectiveUsd(model.pricing.prompt, model); const outputCost = effectiveUsd(model.pricing.completion, model); if (inputCost === undefined || outputCost === undefined) { @@ -401,7 +450,7 @@ export function buildNeosantaraModel( { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoning ? neosantaraReasoningControls(model.id, baseModel) : undefined, + reasoning_options: reasoningOptions, // Reasoning is streamed back in `reasoning_content`, whichever lab built the model. interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: model.capabilities.includes("vision"), @@ -467,9 +516,7 @@ export const neosantara = { return { id: model.id, model: translated, - header: translated.reasoning_options !== undefined - ? neosantaraReasoningHeader(translated.reasoning_options) - : undefined, + header: neosantaraModelHeader(model, translated.reasoning_options), }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 00973d343ae..055354165e1 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -6,6 +6,7 @@ import { buildNeosantaraModel, mergeNeosantaraCatalogs, neosantara, + neosantaraFxHeader, neosantaraInputModalities, neosantaraReasoningControls, neosantaraReasoningHeader, @@ -115,6 +116,8 @@ test("parses and merges both public Neosantara endpoints", () => { expect(merged).toHaveLength(4); expect(merged[0]?.exchange_rate).toBe(20_000); + expect(merged[0]?.exchange_rate_source).toBe("test"); + expect(merged[0]?.exchange_rate_updated_at).toBe("2026-09-02T16:32:29.553Z"); expect(merged[0]?.pricing.prompt).toBe(1.5); }); @@ -160,6 +163,26 @@ test("builds override-only models with effective USD pricing and host reasoning expect(idr.interleaved).toBeUndefined(); }); +test("writes FX provenance only for IDR-derived USD prices", () => { + const merged = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + ); + const usd = merged[0]!; + const idr = merged[1]!; + + expect(neosantaraFxHeader(usd)).toBeUndefined(); + expect(neosantaraFxHeader(idr)).toBe( + "# FX: IDR prices converted to USD at 1 USD = 20000 IDR (test, 2026-09-02).\n", + ); + + const translated = neosantara.translateModel(idr, { + existing: () => undefined, + authored: () => undefined, + }); + expect(translated?.header).toBe(neosantaraFxHeader(idr)); +}); + test("never advertises a control this host cannot send", () => { const source = mergeNeosantaraCatalogs( NeosantaraModelsResponse.parse(modelsResponse), @@ -169,13 +192,14 @@ test("never advertises a control this host cannot send", () => { const ids = [ ["gpt-5.6-terra", "openai/gpt-5.6-terra"], ["claude-sonnet-5", "anthropic/claude-sonnet-5"], - ["deepseek-v4-pro", "deepseek/deepseek-v4-pro"], ["glm-5.3-flash", "zhipuai/glm-5.3-flash"], ["gemini-3.7-flash", "google/gemini-3.7-flash"], ] as const; for (const [id, base] of ids) { const options = neosantaraReasoningControls(id, base); + expect(options).toBeDefined(); + if (options === undefined) continue; // Neither a thinking budget nor a bare toggle names a field a caller can use here. expect(options.some((option) => option.type === "budget_tokens")).toBe(false); expect(options.some((option) => option.type === "toggle")).toBe(false); @@ -473,10 +497,13 @@ test("derives reasoning controls from the canonical tree so new models need no c { type: "effort", values: ["low", "medium", "high", "xhigh"] }, ]); - // A lab toggle is this host's `reasoning_effort = none`, so it joins the effort list. - expect(neosantaraReasoningControls("deepseek-v4-pro", "deepseek/deepseek-v4-pro")).toEqual([ - { type: "effort", values: ["none", "high"] }, - ]); + // DeepSeek's backend route only distinguishes `none` from non-`none`; it does not forward a + // graded effort upstream, so neither lab `max` nor the projected `high` is advertised. + for (const base of ["deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash"] as const) { + const controls = neosantaraReasoningControls(base.split("/")[1]!, base); + expect(controls).toEqual([{ type: "toggle" }]); + expect(neosantaraReasoningHeader(controls!)).toContain("Omitting the field defaults to off"); + } // An always-on reasoner keeps an empty control set rather than borrowing a peer's levels. expect(neosantaraReasoningControls("kimi-k2-thinking", "moonshotai/kimi-k2-thinking")).toEqual([]); @@ -492,9 +519,30 @@ test("derives reasoning controls from the canonical tree so new models need no c expect(neosantaraReasoningHeader(controls)).toContain("reasoning_effort"); } - // A dated snapshot resolves to its lab entry rather than falling through to peers. + // A dated snapshot uses the same binary DeepSeek route. expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) - .toEqual([{ type: "effort", values: ["none", "high"] }]); + .toEqual([{ type: "toggle" }]); +}); + +test("does not turn unresolved relay controls into always-on", () => { + expect(neosantaraReasoningControls("future-model", "example/future-model")).toBeUndefined(); + expect(neosantaraReasoningControls("devstral-2", "mistral/devstral-2512")).toBeUndefined(); + + const source = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!; + const unresolved = { + ...source, + id: "devstral-2", + capabilities: ["text_generation", "function_calling", "reasoning"], + }; + expect(shouldSyncNeosantaraModel(unresolved)).toBe(false); + expect(neosantara.translateModel(unresolved, { + existing: () => undefined, + authored: () => undefined, + })).toBeUndefined(); + expect(neosantara.sourceID(unresolved)).toBe("devstral-2"); }); test("falls through to peers when the lab documents only a control this host cannot send", () => { @@ -502,6 +550,8 @@ test("falls through to peers when the lab documents only a control this host can // set. That is unknown, not always-on, so peer consensus decides instead of publishing `[]`. const controls = neosantaraReasoningControls("claude-4.5-sonnet", "anthropic/claude-sonnet-4-5"); + expect(controls).toBeDefined(); + if (controls === undefined) return; expect(controls.length).toBeGreaterThan(0); expect(controls.some((option) => option.type === "budget_tokens")).toBe(false); for (const value of controls.flatMap((option) => ("values" in option ? option.values : []))) { diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index db116c8d3c3..2b7fb14e851 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,3 +1,7 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value turns it on. Omitting the field defaults to off. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false @@ -6,8 +10,7 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "low", "high"] +type = "toggle" [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 98bea81ecc8..a6bef95cc98 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,11 +1,14 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value turns it on. Omitting the field defaults to off. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro-0813" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "high"] +type = "toggle" [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 91515a4140a..c83ae536730 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,3 +1,7 @@ +# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted +# value turns it on. Omitting the field defaults to off. This host has no separate on/off +# field: reasoning.enabled alone does not enable reasoning. +# https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false @@ -6,8 +10,7 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "high"] +type = "toggle" [cost] input = 2.4 diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml index f7cecfcb7a0..b300dd12ab1 100644 --- a/providers/neosantara/models/glm-4.5-flash.toml +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "zhipuai/glm-4.5-flash" attachment = true reasoning = false diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 6c97984a139..254a64f4b6a 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,5 +1,6 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "zhipuai/glm-4.6v-flash" diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml index 2ee2012297f..e0094620bac 100644 --- a/providers/neosantara/models/glm-4.7.toml +++ b/providers/neosantara/models/glm-4.7.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "zhipuai/glm-4.7" attachment = true reasoning = false diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index 1d8b65e0bc4..81bfa4807e6 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "openai/gpt-5-nano" base_model_omit = ["limit.input"] diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index 0c7f54843de..e24464d7661 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "openai/gpt-5.4-nano" [interleaved] diff --git a/providers/neosantara/models/gpt-oss-20b.toml b/providers/neosantara/models/gpt-oss-20b.toml index 89f50478552..2b06c8e0117 100644 --- a/providers/neosantara/models/gpt-oss-20b.toml +++ b/providers/neosantara/models/gpt-oss-20b.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "openai/gpt-oss-20b" reasoning = false diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 714b06b332d..d161dde7b66 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.5" diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 4d64e49ae70..a2b74e1af94 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.6" diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index 00037b1bdb5..dfd7f9672c9 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index e66616b98bc..f0c215c5618 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 08009ad97c3..7fe6199f3f7 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value (or omitting the field) leaves it on. This host has no separate on/off +# value turns it on. Omitting the field defaults to off. This host has no separate on/off # field: reasoning.enabled alone does not enable reasoning. # https://docs.neosantara.xyz base_model = "inclusionai/ling-3.0-flash-fin" diff --git a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml index 0f540b71f9c..98845e4a200 100644 --- a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml +++ b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "nvidia/llama-3.3-nemotron-super-49b-v1.5" reasoning = false structured_output = true diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 2c5cfb3a8bf..926dd2ed959 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,3 +1,4 @@ +# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "stepfun/step-3.5-flash" structured_output = true diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index d50f9212efa..fb08c9f10f6 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -7,7 +7,9 @@ # Effort: reasoning_effort = none|minimal|low|medium|high|xhigh # Off is reasoning_effort = none; anything else turns reasoning on. # Binary on/off controls are authored as `toggle`, where reasoning_effort = "none" -# disables reasoning and any other accepted value (or omitting the field) leaves it on. +# disables reasoning, another accepted value enables it, and omission defaults to off. +# DeepSeek routes are binary: the backend consumes a non-none effort only as an enable flag +# and forwards no grade, so their lab high/max distinction is intentionally not published. # A model without the `reasoning` capability rejects the field with HTTP 400. # # Capability flags (reasoning, vision, json_mode, ...) are read from each catalog entry's From b02ee205a340d4ad1b4e3316a56a0a4024088e0e Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 13:09:05 +0000 Subject: [PATCH 14/29] fix(neosantara): advertise the host's mapped reasoning effort enum uniformly Neosantara is a relay that normalizes every model's reasoning onto one OpenAI-compatible reasoning_effort field and maps each upstream native shape (Anthropic budgets, DeepSeek/GLM toggles, OpenAI effort) behind it. Its catalog advertises only whether a model reasons, not per-model effort levels, so stop deriving per-model control sets from lab/peer files and advertise the host's accepted enum for every reasoning-capable model. This removes the lab/peer indexing, the DeepSeek toggle special-case, and the always-on [] path. No reasoning model publishes [] anymore; DeepSeek is treated like every other relayed model since the gateway maps effort. Each generated reasoning TOML now carries a leading note explaining that the effort list is Neosantara's mapped request surface, not the upstream provider's API, so reviewers understand the divergence. Verified live via the AI SDK (streaming): the host accepts none|minimal|low|medium|high|xhigh and rejects max (HTTP 400). Future-proof: an optional catalog reasoning_efforts array is consumed automatically (filtered to the host enum) when Neosantara starts publishing per-model levels; until then the full enum is used. Capability flags remain catalog-driven, so a model the lab ships as a reasoner stays reasoning=false until the neosantara catalog advertises it. --- .../core/src/sync/providers/neosantara.ts | 228 ++++-------------- packages/core/test/neosantara-sync.test.ts | 149 +++++------- .../neosantara/models/claude-fable-5.toml | 8 +- .../neosantara/models/claude-opus-4-6.toml | 8 +- .../neosantara/models/claude-opus-5.toml | 8 +- .../neosantara/models/claude-opus-7.toml | 8 +- .../neosantara/models/claude-opus-8.toml | 8 +- .../neosantara/models/claude-sonnet-4-6.toml | 8 +- .../neosantara/models/claude-sonnet-5.toml | 8 +- .../neosantara/models/deepseek-v4-flash.toml | 13 +- .../models/deepseek-v4-pro-0813.toml | 13 +- .../neosantara/models/deepseek-v4-pro.toml | 13 +- .../neosantara/models/gemini-3.6-flash.toml | 8 +- .../neosantara/models/gemini-3.7-flash.toml | 8 +- .../neosantara/models/glm-4.6v-flash.toml | 13 +- .../neosantara/models/glm-5.3-flash.toml | 8 +- providers/neosantara/models/gpt-5-nano.toml | 8 +- providers/neosantara/models/gpt-5.4-nano.toml | 8 +- providers/neosantara/models/gpt-5.4.toml | 8 +- providers/neosantara/models/gpt-5.5.toml | 8 +- providers/neosantara/models/gpt-5.6-luna.toml | 8 +- providers/neosantara/models/gpt-5.6-sol.toml | 8 +- .../neosantara/models/gpt-5.6-terra.toml | 8 +- .../neosantara/models/kimi-k2-thinking.toml | 11 +- providers/neosantara/models/kimi-k2.5.toml | 13 +- providers/neosantara/models/kimi-k2.6.toml | 13 +- providers/neosantara/models/kimi-k3.toml | 8 +- providers/neosantara/models/laguna-s-2.1.toml | 13 +- .../neosantara/models/laguna-xs-2.1.toml | 13 +- .../neosantara/models/ling-3.0-flash-fin.toml | 13 +- providers/neosantara/models/minimax-m2.7.toml | 11 +- .../neosantara/models/muse-spark-1.1.toml | 8 +- .../neosantara/models/step-3.5-flash.toml | 8 +- providers/neosantara/provider.toml | 20 +- 34 files changed, 345 insertions(+), 351 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 5f14da4c027..78069507339 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -1,6 +1,3 @@ -import { readdirSync, readFileSync } from "node:fs"; -import path from "node:path"; - import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; @@ -8,9 +5,6 @@ import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js" const MODELS_ENDPOINT = "https://api.neosantara.xyz/v1/models"; const PRICING_ENDPOINT = "https://api.neosantara.xyz/v1/public/pricing"; const MIN_CONTEXT_WINDOW = 100_000; -// Resolved from this module, not the working directory: the tree is what every reasoning -// control is derived from, so it must not depend on where the sync happens to be launched. -const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers"); const Pricing = z.object({ currency: z.enum(["USD", "IDR"]), @@ -31,6 +25,9 @@ export const NeosantaraModel = z.object({ pricing: Pricing, discount: z.number().min(0).max(100).optional(), capabilities: z.array(z.string()), + // Future-proofing: Neosantara does not publish per-model effort levels today. When it does, + // this optional array is consumed automatically (see neosantaraReasoningEfforts). + reasoning_efforts: z.array(z.string()).optional(), deprecated: z.boolean(), deprecation_alternatives: z.array(z.string()), }).passthrough(); @@ -145,183 +142,61 @@ type ReasoningControls = NonNullable; /** * Reasoning controls (AGENTS.md → "Reasoning options"). * - * This host is OpenAI-compatible and normalizes reasoning onto a single caller-facing - * field, `reasoning_effort`, accepting the values below. Upstream-native shapes (thinking - * budgets, vendor toggles) are handled behind that field and never appear in a caller's - * request, so the control is always an effort list. + * Neosantara is a relay. It normalizes every upstream model's reasoning onto a single + * OpenAI-compatible `reasoning_effort` field and maps each native shape (Anthropic thinking + * budgets, DeepSeek/GLM toggles, OpenAI effort, …) behind it, so a caller never sends an + * upstream-native control here. Its catalog advertises only *whether* a model reasons, never + * per-model effort granularity, so this module does not derive per-model control sets from lab + * or peer files — every reasoning-capable model advertises the host's accepted effort enum. + * + * `HOST_EFFORTS` is a host fact (the request schema's enum), not a per-model choice. Verified + * live via the AI SDK: `max` is rejected (HTTP 400: `none|minimal|low|medium|high|xhigh`) while + * those six are accepted. `none` is the off value for models that support switching reasoning + * off; always-on models ignore it upstream but still accept it on the wire. * - * The values are read from the canonical tree at sync time rather than hardcoded: the - * underlying lab entry wins, otherwise the set its same-surface peers agree on, and the - * result is intersected with what this host accepts — which is why `max` never appears. - * A model whose lab and peers document no graded level at all has only an on/off choice, - * which is a toggle. New models therefore need no change here. + * Future-proof: if the catalog begins publishing per-model effort levels (optional + * `reasoning_efforts`), they are honored automatically after filtering to the host enum; + * until then the full enum is advertised. New models — reasoning or not — need no code change. */ -const HOST_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]); +const HOST_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const; +const HOST_EFFORT_SET: ReadonlySet = new Set(HOST_EFFORTS); -/** A model that reasons with no caller control. */ -const ALWAYS_ON = "always-on"; /** - * A model whose only caller choice is whether to reason at all. The switch is - * `reasoning_effort`, not a separate field — `reasoning.enabled` on its own is inert — so the - * control is a toggle and the header below records the mapping. + * Leading note stamped on every generated reasoning TOML. It exists so a reviewer comparing + * this file to the model's own lab entry or another provider understands the divergence: the + * effort list is Neosantara's mapped request surface, not the upstream provider's native API. */ -const ON_OFF_ONLY = "on-off"; - -const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +const REASONING_NOTE = `# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible \`reasoning_effort\` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz `; -function tomlFilesIn(dir: string): string[] { - let entries; - try { - entries = readdirSync(dir, { withFileTypes: true }); - } catch { - return []; - } - return entries.flatMap((entry) => - entry.isDirectory() - ? tomlFilesIn(path.join(dir, entry.name)) - : entry.name.endsWith(".toml") - ? [path.join(dir, entry.name)] - : [], - ); -} - -function parseToml(filePath: string) { - try { - return Bun.TOML.parse(readFileSync(filePath, "utf8")) as Record; - } catch { - return undefined; - } -} - /** - * Project another entry's controls onto this host's single field. - * - * An empty set means always-on. A lab-side toggle is `reasoning_effort = none` here, so it - * joins the effort list rather than staying a separate option — a bare toggle would claim an - * on/off field this host does not have. Values the host cannot send are dropped. + * Effort levels this host accepts for a model: catalog-advertised when present (future-proofing + * for when Neosantara starts publishing them), otherwise the full host enum, because today the + * catalog exposes reasoning capability but not grades. */ -function hostControls( - options: unknown, -): string[] | typeof ALWAYS_ON | typeof ON_OFF_ONLY | undefined { - if (!Array.isArray(options)) return undefined; - if (options.length === 0) return ALWAYS_ON; - - let toggled = false; - let accepted: string[] | undefined; - for (const option of options) { - if (typeof option !== "object" || option === null) continue; - const type = (option as { type?: unknown }).type; - if (type === "toggle") toggled = true; - if (type !== "effort") continue; - const values = (option as { values?: unknown }).values; - if (!Array.isArray(values)) continue; - const usable = values.filter( - (value): value is string => typeof value === "string" && HOST_EFFORTS.has(value), - ); - if (usable.length > 0) accepted = usable; - } - - if (accepted === undefined) return toggled ? ON_OFF_ONLY : undefined; - return toggled && !accepted.includes("none") ? ["none", ...accepted] : accepted; +function neosantaraReasoningEfforts(model: NeosantaraSourceModel): string[] { + const advertised = Array.isArray(model.reasoning_efforts) + ? model.reasoning_efforts.filter( + (effort): effort is string => typeof effort === "string" && HOST_EFFORT_SET.has(effort), + ) + : []; + return advertised.length > 0 ? advertised : [...HOST_EFFORTS]; } -let controlIndex: { lab: Map>; peers: Map> } | undefined; - -/** Index every other provider's controls: the model's own lab, then peer consensus. */ -function indexControls() { - if (controlIndex !== undefined) return controlIndex; - - const lab = new Map>(); - const tally = new Map>(); - const shapes = new Map>>(); - let parsed = 0; - - for (const file of tomlFilesIn(PROVIDERS_DIR)) { - if (file.startsWith(path.join(PROVIDERS_DIR, "neosantara"))) continue; - const toml = parseToml(file); - if (toml === undefined) continue; - parsed++; - const controls = hostControls(toml.reasoning_options); - - // A lab's own directory is authoritative, wherever the file sits inside it. An entry whose - // controls cannot be read is left unrecorded so peer consensus still gets a say; recording - // it as `undefined` would claim the lab documents no control and settle for always-on. - const owner = path.relative(PROVIDERS_DIR, file).split(path.sep)[0]; - const name = path.basename(file, ".toml"); - const labKey = `${owner}/${name}`; - if (owner !== undefined && controls !== undefined && !lab.has(labKey)) lab.set(labKey, controls); - - const base = toml.base_model; - if (typeof base !== "string" || controls === undefined) continue; - const key = typeof controls === "string" ? controls : controls.join(","); - const counts = tally.get(base) ?? new Map(); - counts.set(key, (counts.get(key) ?? 0) + 1); - tally.set(base, counts); - const seen = shapes.get(base) ?? new Map(); - seen.set(key, controls); - shapes.set(base, seen); - } - - // Every control on this host is derived from the tree, so an unreadable tree is not an empty - // answer — it would publish "no caller control" for every reasoning model. Fail loudly: - // the causes are an incomplete checkout or a runtime whose `Bun.TOML` cannot parse the files. - if (parsed === 0) { - throw new Error( - `Neosantara reasoning controls need the provider tree, but no TOML under '${PROVIDERS_DIR}' could be read. Run the sync from a full checkout with Bun.`, - ); - } - - const peers = new Map>(); - for (const [base, counts] of tally) { - const [best] = [...counts.entries()].sort((a, b) => b[1] - a[1]); - if (best !== undefined) peers.set(base, shapes.get(base)?.get(best[0])); - } - - controlIndex = { lab, peers }; - return controlIndex; +/** Uniform for every reasoning-capable model, including relayed DeepSeek: the host maps effort, + * so the same request surface applies regardless of the upstream provider's native control. */ +export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { + return [{ type: "effort", values: neosantaraReasoningEfforts(model) as never }]; } -/** - * Reasoning controls (AGENTS.md → "Reasoning options"). - * - * This host is OpenAI-compatible and normalizes reasoning onto one caller-facing field, - * `reasoning_effort`. Upstream-native shapes never reach a caller, so controls are read from - * the canonical tree at sync time instead of being listed here: the model's own lab entry - * wins, otherwise the shape its peers agree on. New models need no change here. - */ -/** A toggle control must carry a leading comment naming the field it refers to. */ -export function neosantaraReasoningHeader(controls: ReasoningControls) { - return controls.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; -} - -export function neosantaraReasoningControls( - id: string, - baseModel: string, -): ReasoningControls | undefined { - const [owner, ...rest] = baseModel.split("/"); - - // DeepSeek routes are binary on this gateway. The backend consumes any non-`none` effort only - // as `reasoning.enabled = true`; DeepSeekProvider forwards no graded effort upstream. Thus the - // lab's high/max distinction cannot be represented here and must not be advertised. This is a - // family-level request-shape rule, not a capability override: the live catalog still decides - // which individual DeepSeek model IDs reason at all. - if (owner === "deepseek") return [{ type: "toggle" }]; - - const { lab, peers } = indexControls(); - const name = rest.at(-1) ?? ""; - // A dated snapshot such as `-0813` shares its lab entry with the undated model. - const candidates = [name, name.replace(/-\d{4,}$/, "")]; - const key = candidates.map((candidate) => `${owner}/${candidate}`).find((k) => lab.has(k)); - const derived = key === undefined ? peers.get(baseModel) : lab.get(key); - - if (derived === undefined) return undefined; - if (derived === ALWAYS_ON) return []; - if (derived === ON_OFF_ONLY) return [{ type: "toggle" }]; - return [{ type: "effort", values: derived as never }]; +/** Reasoning TOMLs carry the note above; non-reasoning models carry none. */ +export function neosantaraReasoningHeader(controls: ReasoningControls | undefined) { + return controls === undefined ? undefined : REASONING_NOTE; } @@ -372,12 +247,6 @@ export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { if (isImageModel(model)) return true; // Skip rather than throw: one missing catalog field must cost a single model, not the run. if (model.pricing.prompt === undefined || model.pricing.completion === undefined) return false; - // Empty means explicitly always-on; undefined means neither the lab nor peers document a - // control. Never turn uncertainty into an affirmative no-control claim on a relay. - if ( - model.capabilities.includes("reasoning") - && neosantaraReasoningControls(model.id, baseModel) === undefined - ) return false; return true; } @@ -427,12 +296,7 @@ export function buildNeosantaraModel( // underlying model can do elsewhere. Reading them live means a capability the host adds later // is picked up by the next sync with no code change. const reasoning = model.capabilities.includes("reasoning"); - const reasoningOptions = reasoning - ? neosantaraReasoningControls(model.id, baseModel) - : undefined; - if (reasoning && reasoningOptions === undefined) { - throw new Error(`No reviewed reasoning controls for Neosantara model '${model.id}'`); - } + const reasoningOptions = reasoning ? neosantaraReasoningControls(model) : undefined; const inputCost = effectiveUsd(model.pricing.prompt, model); const outputCost = effectiveUsd(model.pricing.completion, model); if (inputCost === undefined || outputCost === undefined) { diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 055354165e1..f8409690c77 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -1,6 +1,4 @@ import { expect, test } from "bun:test"; -import { tmpdir } from "node:os"; -import path from "node:path"; import { buildNeosantaraModel, @@ -143,7 +141,10 @@ test("builds override-only models with effective USD pricing and host reasoning const gemini = buildNeosantaraModel(merged[0]!, undefined); expect(gemini).toMatchObject({ base_model: "google/gemini-3.7-flash", - reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }], + // Uniform host effort enum — the catalog exposes reasoning capability, not grades. + reasoning_options: [ + { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, + ], // This host streams reasoning in `reasoning_content`. interleaved: { field: "reasoning_content" }, limit: { context: 1_000_000 }, @@ -189,30 +190,22 @@ test("never advertises a control this host cannot send", () => { NeosantaraPricingResponse.parse(pricingResponse), )[0]!; - const ids = [ - ["gpt-5.6-terra", "openai/gpt-5.6-terra"], - ["claude-sonnet-5", "anthropic/claude-sonnet-5"], - ["glm-5.3-flash", "zhipuai/glm-5.3-flash"], - ["gemini-3.7-flash", "google/gemini-3.7-flash"], - ] as const; - - for (const [id, base] of ids) { - const options = neosantaraReasoningControls(id, base); - expect(options).toBeDefined(); - if (options === undefined) continue; - // Neither a thinking budget nor a bare toggle names a field a caller can use here. + // Whatever the upstream lab, the host maps effort onto the same accepted enum: only effort, + // no budget_tokens or toggle, and every value inside the host enum. + for (const id of ["gpt-5.6-terra", "claude-sonnet-5", "glm-5.3-flash", "gemini-3.7-flash"]) { + const options = neosantaraReasoningControls({ ...source, id }); expect(options.some((option) => option.type === "budget_tokens")).toBe(false); expect(options.some((option) => option.type === "toggle")).toBe(false); - expect(neosantaraReasoningHeader(options)).toBeUndefined(); + expect(neosantaraReasoningHeader(options)).toContain("reasoning_effort"); for (const value of options.flatMap((o) => ("values" in o ? o.values : []))) { expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); } } - // The request enum is never dumped wholesale onto a model. + // The full host enum is what a reasoning model advertises, including the `none` off value. const built = buildNeosantaraModel({ ...source, id: "gpt-5.6-terra" }, undefined); expect(built.reasoning_options).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, ]); }); @@ -488,93 +481,63 @@ test("skips a model with unusable upstream pricing instead of aborting the whole expect(neosantara.sourceID(merged[0]!)).toBe("kimi-k2.5"); }); -test("derives reasoning controls from the canonical tree so new models need no code change", () => { - // Lab entry wins, intersected with what this host accepts, so `max` never survives. - expect(neosantaraReasoningControls("gpt-5.6-terra", "openai/gpt-5.6-terra")).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, - ]); - expect(neosantaraReasoningControls("claude-fable-5", "anthropic/claude-fable-5")).toEqual([ - { type: "effort", values: ["low", "medium", "high", "xhigh"] }, - ]); - - // DeepSeek's backend route only distinguishes `none` from non-`none`; it does not forward a - // graded effort upstream, so neither lab `max` nor the projected `high` is advertised. - for (const base of ["deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash"] as const) { - const controls = neosantaraReasoningControls(base.split("/")[1]!, base); - expect(controls).toEqual([{ type: "toggle" }]); - expect(neosantaraReasoningHeader(controls!)).toContain("Omitting the field defaults to off"); +test("advertises the host effort enum uniformly for every reasoning model", () => { + const source = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!; + const enumEffort = [ + { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, + ]; + + // Anthropic (lab uses budgets), OpenAI (native effort), and relayed DeepSeek (lab toggle) all + // resolve to the same mapped surface — the module reads capability, not lab shape. + for (const id of ["claude-fable-5", "gpt-5.6-terra", "deepseek-v4-pro", "deepseek-v4-flash"]) { + expect(neosantaraReasoningControls({ ...source, id })).toEqual(enumEffort); } +}); - // An always-on reasoner keeps an empty control set rather than borrowing a peer's levels. - expect(neosantaraReasoningControls("kimi-k2-thinking", "moonshotai/kimi-k2-thinking")).toEqual([]); - - // A lab with no graded level is a binary control, carried with a wire comment rather than - // an effort list that could only say how to switch reasoning off. - for (const [id, base] of [ - ["kimi-k2.5", "moonshotai/kimi-k2.5"], - ["laguna-s-2.1", "poolside/laguna-s-2.1"], - ] as const) { - const controls = neosantaraReasoningControls(id, base); - expect(controls).toEqual([{ type: "toggle" }]); - expect(neosantaraReasoningHeader(controls)).toContain("reasoning_effort"); - } +test("honors catalog-published effort levels when they appear, filtered to the host enum", () => { + const source = mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[0]!; - // A dated snapshot uses the same binary DeepSeek route. - expect(neosantaraReasoningControls("deepseek-v4-pro-0813", "deepseek/deepseek-v4-pro-0813")) - .toEqual([{ type: "toggle" }]); -}); + // Future catalog: a per-model effort list is honored, and values the host cannot send (`max`) + // are dropped rather than published. + const future = { ...source, reasoning_efforts: ["low", "high", "max"] }; + expect(neosantaraReasoningControls(future)).toEqual([ + { type: "effort", values: ["low", "high"] }, + ]); -test("does not turn unresolved relay controls into always-on", () => { - expect(neosantaraReasoningControls("future-model", "example/future-model")).toBeUndefined(); - expect(neosantaraReasoningControls("devstral-2", "mistral/devstral-2512")).toBeUndefined(); + // An empty or all-invalid catalog list falls back to the full host enum, never to `[]`. + expect(neosantaraReasoningControls({ ...source, reasoning_efforts: ["max"] })).toEqual([ + { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, + ]); +}); +test("stamps every reasoning TOML with the note explaining the mapped surface", () => { const source = mergeNeosantaraCatalogs( NeosantaraModelsResponse.parse(modelsResponse), NeosantaraPricingResponse.parse(pricingResponse), )[0]!; - const unresolved = { - ...source, - id: "devstral-2", - capabilities: ["text_generation", "function_calling", "reasoning"], - }; - expect(shouldSyncNeosantaraModel(unresolved)).toBe(false); - expect(neosantara.translateModel(unresolved, { - existing: () => undefined, - authored: () => undefined, - })).toBeUndefined(); - expect(neosantara.sourceID(unresolved)).toBe("devstral-2"); -}); + const context = { existing: () => undefined, authored: () => undefined }; -test("falls through to peers when the lab documents only a control this host cannot send", () => { - // Anthropic's own entry for this model is a thinking budget, which no caller of this host can - // set. That is unknown, not always-on, so peer consensus decides instead of publishing `[]`. - const controls = neosantaraReasoningControls("claude-4.5-sonnet", "anthropic/claude-sonnet-4-5"); - - expect(controls).toBeDefined(); - if (controls === undefined) return; - expect(controls.length).toBeGreaterThan(0); - expect(controls.some((option) => option.type === "budget_tokens")).toBe(false); - for (const value of controls.flatMap((option) => ("values" in option ? option.values : []))) { - expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); - } -}); + // A reasoning model (gemini here) carries the note so a reviewer sees why the shape differs + // from the upstream provider's own entry. + const reasoning = neosantara.translateModel(source, context); + expect(reasoning?.header).toContain("Neosantara relays this model"); + expect(reasoning?.header).toContain("reasoning_effort"); -test("reads the provider tree from the module, not the working directory", () => { - const module = path.join(import.meta.dir, "..", "src", "sync", "providers", "neosantara.ts"); - const run = Bun.spawnSync( - [ - "bun", - "-e", - `const { neosantaraReasoningControls } = await import(${JSON.stringify(module)}); - console.log(JSON.stringify(neosantaraReasoningControls("gpt-5.4", "openai/gpt-5.4")));`, - ], - { cwd: tmpdir() }, + // A non-reasoning model carries no reasoning note. + const plain = neosantara.translateModel( + mergeNeosantaraCatalogs( + NeosantaraModelsResponse.parse(modelsResponse), + NeosantaraPricingResponse.parse(pricingResponse), + )[1]!, + context, ); - - expect(run.stderr.toString()).toBe(""); - expect(JSON.parse(run.stdout.toString())).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, - ]); + expect(plain?.header ?? "").not.toContain("Neosantara relays this model"); }); test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 5b43c0b5381..9902c74ffd3 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-fable-5" attachment = false structured_output = true @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 2ee956d624e..b2340733b61 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-4-6" attachment = false structured_output = true @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 52917a6e0da..d6a62587cd4 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-5" structured_output = false @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-7.toml b/providers/neosantara/models/claude-opus-7.toml index 3bd4e68f667..0f5cf1f3d1f 100644 --- a/providers/neosantara/models/claude-opus-7.toml +++ b/providers/neosantara/models/claude-opus-7.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-4-7" structured_output = true @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-8.toml b/providers/neosantara/models/claude-opus-8.toml index c5fe963c3ed..20e671e3aa8 100644 --- a/providers/neosantara/models/claude-opus-8.toml +++ b/providers/neosantara/models/claude-opus-8.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-4-8" structured_output = true @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 1b84030982c..bb9b094d099 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-sonnet-4-6" attachment = false structured_output = true @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index 4478b6ab406..e0f816fddaf 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-sonnet-5" structured_output = true @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 2b7fb14e851..d464b1dd8ab 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,7 +1,9 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false @@ -10,7 +12,8 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index a6bef95cc98..8a827ad198d 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,14 +1,17 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro-0813" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index c83ae536730..315beb4f259 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,7 +1,9 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false @@ -10,7 +12,8 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 2.4 diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 4065d09c643..3c47bf89fdb 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "google/gemini-3.6-flash" [interleaved] @@ -5,7 +11,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["minimal", "low", "medium", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.75 diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 46aa16386a5..3322ced4ce0 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "google/gemini-3.7-flash" [interleaved] @@ -5,7 +11,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.75 diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 254a64f4b6a..548553f8c34 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,8 +1,10 @@ # FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "zhipuai/glm-4.6v-flash" structured_output = false @@ -10,7 +12,8 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.015625 diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml index b1e7437b7ac..bd43fadb0fd 100644 --- a/providers/neosantara/models/glm-5.3-flash.toml +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "zhipuai/glm-5.3-flash" attachment = false @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.15 diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index 81bfa4807e6..34b23c57090 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -1,4 +1,10 @@ # FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5-nano" base_model_omit = ["limit.input"] @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["minimal", "low", "medium", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.050625 diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index e24464d7661..3362c787fd9 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -1,4 +1,10 @@ # FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.4-nano" [interleaved] @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.2025 diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 67d20995843..7e16b2cdd19 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.4" base_model_omit = ["limit.input"] attachment = false @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 2.5 diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index 3b86415307d..602dec83cf1 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.5" base_model_omit = ["limit.input"] attachment = false @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml index fd02a7e98dc..5aad56d5558 100644 --- a/providers/neosantara/models/gpt-5.6-luna.toml +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-luna" base_model_omit = ["limit.input"] attachment = false @@ -8,7 +14,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.2 diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml index 0f493c7de28..485b0d09607 100644 --- a/providers/neosantara/models/gpt-5.6-sol.toml +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-sol" base_model_omit = ["limit.input"] attachment = false @@ -8,7 +14,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index e485bb96b90..abe9bbd4a10 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-terra" base_model_omit = ["limit.input"] structured_output = false @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 2 diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index 62d92c7bac9..bb93a54fcd2 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,10 +1,19 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2-thinking" structured_output = false -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] + [cost] input = 0.6 output = 2.5 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index d161dde7b66..0c55b384706 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,7 +1,9 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.5" structured_output = false @@ -9,7 +11,8 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index a2b74e1af94..30a6a21d902 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,7 +1,9 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.6" attachment = false @@ -9,7 +11,8 @@ attachment = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.285 diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 86cc82fbb35..089ceb69cce 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k3" attachment = false @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 3 diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index dfd7f9672c9..eda45a05466 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,14 +1,17 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index f0c215c5618..5a5738f3b45 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,14 +1,17 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 7fe6199f3f7..b3a753e2bac 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,7 +1,9 @@ -# Toggle: reasoning_effort = "none" turns reasoning off; any other accepted -# value turns it on. Omitting the field defaults to off. This host has no separate on/off -# field: reasoning.enabled alone does not enable reasoning. -# https://docs.neosantara.xyz +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false @@ -9,7 +11,8 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "toggle" +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index 47f5f97e08f..6faf5ab0d02 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,10 +1,19 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "minimax/MiniMax-M2.7" structured_output = true -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "effort" +values = ["none", "minimal", "low", "medium", "high", "xhigh"] + [cost] input = 0.09 output = 0.36 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index c81c2320be0..e5dfe4493c9 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -1,3 +1,9 @@ +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "meta/muse-spark-1.1" structured_output = false @@ -6,7 +12,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 1.25 diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 926dd2ed959..1486e1838a2 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,4 +1,10 @@ # FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). +# Reasoning: Neosantara relays this model and normalizes reasoning onto a +# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape +# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog +# advertises only that the model reasons, not its effort levels, so this entry lists the host's +# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request +# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "stepfun/step-3.5-flash" structured_output = true @@ -7,7 +13,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "high"] +values = ["none", "minimal", "low", "medium", "high", "xhigh"] [cost] input = 0.1875 diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index fb08c9f10f6..e3d062a36d8 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -3,19 +3,21 @@ # - https://api.neosantara.xyz/v1/models (catalog, public, no key) # - https://api.neosantara.xyz/v1/public/pricing (raw_pricing + meta.exchange_rate, public) # -# Reasoning wire format on POST /v1/chat/completions: -# Effort: reasoning_effort = none|minimal|low|medium|high|xhigh -# Off is reasoning_effort = none; anything else turns reasoning on. -# Binary on/off controls are authored as `toggle`, where reasoning_effort = "none" -# disables reasoning, another accepted value enables it, and omission defaults to off. -# DeepSeek routes are binary: the backend consumes a non-none effort only as an enable flag -# and forwards no grade, so their lab high/max distinction is intentionally not published. -# A model without the `reasoning` capability rejects the field with HTTP 400. +# Reasoning: Neosantara is a relay. It normalizes every model's reasoning onto a single +# OpenAI-compatible `reasoning_effort` field and maps each upstream native shape (Anthropic +# thinking budgets, DeepSeek/GLM toggles, OpenAI effort, …) behind it, so callers never send an +# upstream-native control. Accepted enum (verified live, `max` → HTTP 400): +# reasoning_effort = none|minimal|low|medium|high|xhigh +# The catalog advertises only whether a model reasons, not its effort levels, so every +# reasoning model advertises this full enum rather than a lab- or peer-derived subset; each +# generated reasoning TOML carries a note explaining that divergence. If the catalog later +# publishes per-model effort levels, the sync consumes them automatically. # -# Capability flags (reasoning, vision, json_mode, ...) are read from each catalog entry's +# Capability flags (reasoning, vision, json_mode, …) are read from each catalog entry's # `capabilities` array and never inherited from lab metadata, because this gateway enforces # its own list: a model the lab ships as a reasoner is published here with reasoning = false # until the catalog advertises it, and starts publishing controls on the sync after it does. +# A model without the `reasoning` capability rejects `reasoning_effort` with HTTP 400. # # Pricing: raw_pricing is per million tokens in USD or IDR and excludes `discount`, # which is a percentage applied at billing time. Published costs are therefore From f3bcecb06073a088e55bc2fbd5dfd614e3ea945f Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 17:49:51 +0000 Subject: [PATCH 15/29] feat(neosantara): sync from the /v1/catalog endpoint with per-model reasoning Rework the sync to consume Neosantara's single models.dev / LLM Gateway shaped catalog (/v1/catalog) instead of merging /v1/models + /v1/public/ pricing. The gateway now performs the host-specific work (USD-per-token pricing, capability flags, and the mapped reasoning surface), so the sync is a thin, override-only translation. Reasoning options are per-model, derived from each model's real control surface reported by the catalog and mapped with the LLM Gateway convention: [] = always-on (no caller control), ["none"] = on/off toggle, otherwise graded effort. No more uniform full-enum dump. Efforts are intersected with the host ladder, which now runs up to `max` (GLM-5.x and DeepSeek V4 use it). Toggle entries carry the required leading wire comment. Kept: the 100k+ context / function-calling / image-model filters and the base-model aliasing. Dropped: the two-endpoint merge, IDR conversion, discount handling, and FX header (all now upstream). Regenerated the provider TOMLs; re-running the sync is idempotent and `bun validate` passes. --- .../core/src/sync/providers/neosantara.ts | 395 ++++------- packages/core/test/neosantara-sync.test.ts | 666 +++++------------- .../neosantara/models/claude-fable-5.toml | 8 +- .../neosantara/models/claude-opus-4-6.toml | 8 +- .../neosantara/models/claude-opus-4-7.toml | 21 + .../neosantara/models/claude-opus-4-8.toml | 21 + .../neosantara/models/claude-opus-5.toml | 8 +- .../neosantara/models/claude-opus-7.toml | 27 - .../neosantara/models/claude-opus-8.toml | 27 - .../neosantara/models/claude-sonnet-4-6.toml | 8 +- .../neosantara/models/claude-sonnet-5.toml | 8 +- .../neosantara/models/deepseek-v4-flash.toml | 8 +- .../models/deepseek-v4-pro-0813.toml | 8 +- .../neosantara/models/deepseek-v4-pro.toml | 8 +- .../neosantara/models/gemini-3.5-flash.toml | 8 +- .../neosantara/models/gemini-3.6-flash.toml | 16 +- .../neosantara/models/gemini-3.7-flash.toml | 16 +- .../neosantara/models/glm-4.5-flash.toml | 10 +- .../neosantara/models/glm-4.6v-flash.toml | 12 +- .../neosantara/models/glm-4.7-flash.toml | 5 +- providers/neosantara/models/glm-4.7.toml | 1 - .../neosantara/models/glm-5.3-flash.toml | 8 +- providers/neosantara/models/gpt-5-nano.toml | 7 - providers/neosantara/models/gpt-5.4-nano.toml | 7 - providers/neosantara/models/gpt-5.4.toml | 6 - providers/neosantara/models/gpt-5.5.toml | 6 - providers/neosantara/models/gpt-5.6-luna.toml | 6 - providers/neosantara/models/gpt-5.6-sol.toml | 6 - .../neosantara/models/gpt-5.6-terra.toml | 6 - providers/neosantara/models/gpt-oss-20b.toml | 1 - .../neosantara/models/kimi-k2-thinking.toml | 11 +- providers/neosantara/models/kimi-k2.5.toml | 8 +- providers/neosantara/models/kimi-k2.6.toml | 12 +- providers/neosantara/models/kimi-k3.toml | 8 +- providers/neosantara/models/laguna-s-2.1.toml | 8 +- .../neosantara/models/laguna-xs-2.1.toml | 8 +- .../neosantara/models/ling-3.0-flash-fin.toml | 8 +- .../llama-3.3-nemotron-super-49b-v1.5.toml | 3 +- providers/neosantara/models/minimax-m2.7.toml | 15 +- .../neosantara/models/muse-spark-1.1.toml | 8 +- .../neosantara/models/step-3.5-flash.toml | 9 +- 41 files changed, 413 insertions(+), 1027 deletions(-) create mode 100644 providers/neosantara/models/claude-opus-4-7.toml create mode 100644 providers/neosantara/models/claude-opus-4-8.toml delete mode 100644 providers/neosantara/models/claude-opus-7.toml delete mode 100644 providers/neosantara/models/claude-opus-8.toml diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 78069507339..2a74dc25e13 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -2,99 +2,73 @@ import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; -const MODELS_ENDPOINT = "https://api.neosantara.xyz/v1/models"; -const PRICING_ENDPOINT = "https://api.neosantara.xyz/v1/public/pricing"; +// Neosantara serves a models.dev / LLM Gateway shaped catalog at this single public endpoint. +const CATALOG_ENDPOINT = "https://api.neosantara.xyz/v1/catalog"; const MIN_CONTEXT_WINDOW = 100_000; -const Pricing = z.object({ - currency: z.enum(["USD", "IDR"]), - prompt: z.number().nonnegative().optional(), - completion: z.number().nonnegative().optional(), - cache_read: z.number().nonnegative().optional(), - cache_write: z.number().nonnegative().optional(), -}).passthrough(); - -export const NeosantaraModel = z.object({ - id: z.string(), - object: z.literal("model"), - created: z.number(), - owned_by: z.string(), - description: z.string(), - context_window: z.number().int().nonnegative().nullable(), - max_output_tokens: z.number().int().nonnegative().nullable(), - pricing: Pricing, - discount: z.number().min(0).max(100).optional(), - capabilities: z.array(z.string()), - // Future-proofing: Neosantara does not publish per-model effort levels today. When it does, - // this optional array is consumed automatically (see neosantaraReasoningEfforts). - reasoning_efforts: z.array(z.string()).optional(), - deprecated: z.boolean(), - deprecation_alternatives: z.array(z.string()), -}).passthrough(); - -export const NeosantaraModelsResponse = z.object({ - object: z.literal("list"), - data: z.array(NeosantaraModel), -}).passthrough(); - -const PublicPricing = z.object({ - currency: z.enum(["USD", "IDR"]).optional(), - per_million_tokens: z.object({ - input: z.number().nonnegative().optional(), - output: z.number().nonnegative().optional(), - cache_read: z.number().nonnegative().optional(), - cache_write: z.number().nonnegative().optional(), - }).optional(), -}).passthrough(); - -// Only `name` and `raw_pricing` are consumed. Everything else is descriptive and optional so a -// single omission cannot fail the whole catalog sync. -const PublicPricingModel = z.object({ - name: z.string(), - capabilities: z.array(z.string()).optional(), - context_window: z.number().int().nonnegative().nullable().optional(), - description: z.string().optional(), - raw_pricing: PublicPricing, -}).passthrough(); +// Accepted reasoning_effort enum (up to `max`); efforts from the catalog are intersected with it. +const HOST_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; +const HOST_EFFORT_SET: ReadonlySet = new Set(HOST_EFFORTS); -export const NeosantaraPricingResponse = z.object({ - data: z.array(PublicPricingModel), - meta: z.object({ - count: z.number().int().nonnegative(), - updated_at: z.string(), - exchange_rate: z.object({ - usd_idr: z.number().positive(), - currency: z.literal("IDR"), - source: z.string(), +const CatalogProvider = z + .object({ + providerId: z.string().optional(), + vision: z.boolean().optional(), + tools: z.boolean().optional(), + reasoning: z.boolean().optional(), + reasoning_efforts: z.array(z.string()).optional(), + }) + .passthrough(); + +// Per-token USD strings; "0" is a real price for free models (kept), unknown for cache/reasoning. +const CatalogPricing = z + .object({ + prompt: z.string().optional(), + completion: z.string().optional(), + internal_reasoning: z.string().optional(), + input_cache_read: z.string().optional(), + input_cache_write: z.string().optional(), + }) + .passthrough(); + +export const NeosantaraModel = z + .object({ + id: z.string(), + name: z.string().optional(), + display_name: z.string().optional(), + created: z.number().optional(), + description: z.string().optional(), + family: z.string().optional(), + architecture: z.object({ + input_modalities: z.array(z.string()), + output_modalities: z.array(z.string()), }), - }), -}).passthrough(); - -const NeosantaraCombinedResponse = z.object({ - models: z.unknown(), - pricing: z.unknown(), -}); - -export type NeosantaraSourceModel = z.infer & { - exchange_rate: number; - exchange_rate_source: string; - exchange_rate_updated_at: string; -}; + context_length: z.number().int().nonnegative().optional(), + pricing: CatalogPricing, + supported_parameters: z.array(z.string()).optional(), + structured_outputs: z.boolean().optional(), + providers: z.array(CatalogProvider).min(1), + deprecated: z.boolean().optional(), + }) + .passthrough(); + +export const NeosantaraCatalogResponse = z + .object({ data: z.array(NeosantaraModel) }) + .passthrough(); + +export type NeosantaraSourceModel = z.infer; + +// One mapping per model carries this host's capability flags and reasoning efforts. +function mapping(model: NeosantaraSourceModel) { + return model.providers[0]; +} -/** - * Neosantara serves bare model ids, so the canonical metadata tree resolves most of them on - * its own and newly launched models are picked up without a code change. Only ids the tree - * cannot resolve are aliased here (renamed generations, aliases pointing at another model, - * and vendor-specific suffixes). - */ +// Only ids the canonical tree cannot resolve are aliased (renamed generations, coding aliases). const BASE_MODEL_ALIASES: Record = { "claude-3-haiku": "anthropic/claude-3-haiku-20240307", "claude-4.5-opus": "anthropic/claude-opus-4-5", "claude-4.5-sonnet": "anthropic/claude-sonnet-4-5", - "claude-opus-7": "anthropic/claude-opus-4-7", - "claude-opus-8": "anthropic/claude-opus-4-8", "devstral-2": "mistral/devstral-2512", - // Neosantara routes its coding alias at Grok 4.3. "grok-code-fast": "xai/grok-4.3", "qwen3-235b-wse": "alibaba/qwen3-235b-a22b-instruct-2507", }; @@ -103,167 +77,78 @@ export function resolveNeosantaraBaseModel(id: string) { return BASE_MODEL_ALIASES[id] ?? resolveModelMetadataBaseModel(id); } -export function mergeNeosantaraCatalogs( - models: z.infer, - pricing: z.infer, -): NeosantaraSourceModel[] { - const modelPricing = new Map(models.data.map((model) => [model.id, model.pricing] as const)); - const publicPricing = new Map(pricing.data.map((model) => { - const raw = model.raw_pricing; - const tokens = raw.per_million_tokens; - // Cache rates may only be surfaced by /v1/models. Inherit them so publishing does not - // silently drop cost data, but never across a currency change: the two figures would - // otherwise be mixed into one scale. - const fallback = modelPricing.get(model.name); - const cache = fallback?.currency === raw.currency ? fallback : undefined; - const normalized = - raw.currency === undefined || tokens?.input === undefined || tokens.output === undefined - ? undefined - : { - currency: raw.currency, - prompt: tokens.input, - completion: tokens.output, - cache_read: tokens.cache_read ?? cache?.cache_read, - cache_write: tokens.cache_write ?? cache?.cache_write, - }; - return [model.name, normalized] as const; - })); - return models.data.map((model) => ({ - ...model, - pricing: publicPricing.get(model.id) ?? model.pricing, - exchange_rate: pricing.meta.exchange_rate.usd_idr, - exchange_rate_source: pricing.meta.exchange_rate.source, - exchange_rate_updated_at: pricing.meta.updated_at, - })); -} +// Display names for ids whose canonical entry is a different model (see BASE_MODEL_ALIASES). +const NAME_OVERRIDES: Record = { + "grok-code-fast": "Grok Code Fast", +}; type ReasoningControls = NonNullable; -/** - * Reasoning controls (AGENTS.md → "Reasoning options"). - * - * Neosantara is a relay. It normalizes every upstream model's reasoning onto a single - * OpenAI-compatible `reasoning_effort` field and maps each native shape (Anthropic thinking - * budgets, DeepSeek/GLM toggles, OpenAI effort, …) behind it, so a caller never sends an - * upstream-native control here. Its catalog advertises only *whether* a model reasons, never - * per-model effort granularity, so this module does not derive per-model control sets from lab - * or peer files — every reasoning-capable model advertises the host's accepted effort enum. - * - * `HOST_EFFORTS` is a host fact (the request schema's enum), not a per-model choice. Verified - * live via the AI SDK: `max` is rejected (HTTP 400: `none|minimal|low|medium|high|xhigh`) while - * those six are accepted. `none` is the off value for models that support switching reasoning - * off; always-on models ignore it upstream but still accept it on the wire. - * - * Future-proof: if the catalog begins publishing per-model effort levels (optional - * `reasoning_efforts`), they are honored automatically after filtering to the host enum; - * until then the full enum is advertised. New models — reasoning or not — need no code change. - */ -const HOST_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const; -const HOST_EFFORT_SET: ReadonlySet = new Set(HOST_EFFORTS); - -/** - * Leading note stamped on every generated reasoning TOML. It exists so a reviewer comparing - * this file to the model's own lab entry or another provider understands the divergence: the - * effort list is Neosantara's mapped request surface, not the upstream provider's native API. - */ -const REASONING_NOTE = `# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible \`reasoning_effort\` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz +// Toggle controls carry a leading wire comment naming the off value (AGENTS.md requirement). +const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz `; -/** - * Effort levels this host accepts for a model: catalog-advertised when present (future-proofing - * for when Neosantara starts publishing them), otherwise the full host enum, because today the - * catalog exposes reasoning capability but not grades. - */ -function neosantaraReasoningEfforts(model: NeosantaraSourceModel): string[] { - const advertised = Array.isArray(model.reasoning_efforts) - ? model.reasoning_efforts.filter( - (effort): effort is string => typeof effort === "string" && HOST_EFFORT_SET.has(effort), - ) - : []; - return advertised.length > 0 ? advertised : [...HOST_EFFORTS]; -} - -/** Uniform for every reasoning-capable model, including relayed DeepSeek: the host maps effort, - * so the same request surface applies regardless of the upstream provider's native control. */ +// The catalog reports each model's real effort surface: [] = always-on (no caller control), +// ["none"] = on/off toggle, otherwise the graded levels (intersected with the host enum). export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { - return [{ type: "effort", values: neosantaraReasoningEfforts(model) as never }]; + const efforts = (mapping(model).reasoning_efforts ?? []).filter((effort) => + HOST_EFFORT_SET.has(effort), + ); + if (efforts.length === 0) return []; + if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; + return [{ type: "effort", values: efforts as never }]; } -/** Reasoning TOMLs carry the note above; non-reasoning models carry none. */ +// A toggle needs its wire comment; effort/always-on models carry none. export function neosantaraReasoningHeader(controls: ReasoningControls | undefined) { - return controls === undefined ? undefined : REASONING_NOTE; + return controls?.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; } - -/** Image models are priced per image and carry no context window, so they skip the token filters. */ +// Image models output an image modality, are priced per image, and skip the token filters. function isImageModel(model: NeosantaraSourceModel) { - return model.capabilities.includes("image_generation"); + return model.architecture.output_modalities.includes("image"); } -/** Display names for ids whose canonical entry is a different model (see BASE_MODEL_ALIASES). */ -const NAME_OVERRIDES: Record = { - "grok-code-fast": "Grok Code Fast", -}; +// Per-token USD string -> USD per million tokens. `0` is preserved (free models are real). +function price(value: string | undefined): number | undefined { + if (value === undefined) return undefined; + const number = Number(value); + return Number.isFinite(number) && number >= 0 + ? Math.round(number * 1_000_000_000_000) / 1_000_000 + : undefined; +} -/** Leading provenance for prices converted from IDR into models.dev's required USD units. */ -export function neosantaraFxHeader(model: NeosantaraSourceModel) { - if (model.pricing.currency !== "IDR") return undefined; - const date = model.exchange_rate_updated_at.slice(0, 10); - const source = model.exchange_rate_source.replace(/[\r\n]+/g, " "); - return `# FX: IDR prices converted to USD at 1 USD = ${model.exchange_rate} IDR` + - ` (${source}, ${date}).\n`; +// Cache/reasoning prices report `0` when unknown; never downgrade to a published zero. +function nonZeroPrice(value: string | undefined): number | undefined { + const result = price(value); + return result !== undefined && result > 0 ? result : undefined; } -function neosantaraModelHeader( - model: NeosantaraSourceModel, - controls: ReasoningControls | undefined, -) { - const headers = [ - neosantaraFxHeader(model), - controls === undefined ? undefined : neosantaraReasoningHeader(controls), - ].filter((header): header is string => header !== undefined); - return headers.length === 0 ? undefined : headers.join(""); +const ALLOWED_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]); + +function modalities(values: string[]): string[] { + const mapped = values + .map((value) => value.toLowerCase()) + .map((value) => (value === "file" ? "pdf" : value)) + .filter((value) => ALLOWED_MODALITIES.has(value)); + return [...new Set(mapped.length > 0 ? mapped : ["text"])]; } -/** Models this host publishes at all, regardless of whether we can translate them yet. */ +// Image generators, or 100k+ context function-calling text models. export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { return ( isImageModel(model) || - (model.context_window !== null && - model.context_window >= MIN_CONTEXT_WINDOW && - model.capabilities.includes("function_calling")) + ((model.context_length ?? 0) >= MIN_CONTEXT_WINDOW && mapping(model).tools === true) ); } export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { - const baseModel = resolveNeosantaraBaseModel(model.id); - if (model.deprecated || baseModel === undefined) return false; + if (model.deprecated || resolveNeosantaraBaseModel(model.id) === undefined) return false; if (!meetsNeosantaraPublicFilter(model)) return false; if (isImageModel(model)) return true; - // Skip rather than throw: one missing catalog field must cost a single model, not the run. - if (model.pricing.prompt === undefined || model.pricing.completion === undefined) return false; - return true; -} - -function effectiveUsd(value: number | undefined, model: NeosantaraSourceModel) { - if (value === undefined) return undefined; - const usd = model.pricing.currency === "IDR" ? value / model.exchange_rate : value; - const discount = model.discount === undefined ? 1 : (100 - model.discount) / 100; - return Math.round(usd * discount * 1_000_000) / 1_000_000; -} - -/** Input modalities this host accepts, derived from the advertised understanding capabilities. */ -export function neosantaraInputModalities(capabilities: string[]) { - return [ - "text" as const, - ...(capabilities.includes("vision") ? (["image"] as const) : []), - ...(capabilities.includes("video_understanding") ? (["video"] as const) : []), - ]; + // Skip rather than throw: one missing price costs a single model, not the run. + return price(model.pricing.prompt) !== undefined && price(model.pricing.completion) !== undefined; } export function buildNeosantaraModel( @@ -275,61 +160,54 @@ export function buildNeosantaraModel( throw new Error(`No canonical base model mapping for Neosantara model '${model.id}'`); } - const limit = { - context: model.context_window ?? undefined, - }; + // context_length is 0 for image models; never author limit.context = 0 — inherit the base. + const limit = { context: model.context_length || undefined }; + const baseModelOmit = existing?.base_model === baseModel ? existing.base_model_omit : undefined; - // models.dev has no per-image cost field, so per-image pricing is left unpublished and - // every other trait is inherited from the canonical lab entry. + // Per-image pricing has no models.dev field, so cost is left unpublished and inherited. if (isImageModel(model)) { return factorBaseModel( baseModel, { status: model.deprecated ? "deprecated" : existing?.status }, limit, - existing?.base_model === baseModel ? existing.base_model_omit : undefined, + baseModelOmit, ); } - // Capabilities come from this host's catalog, never from the lab entry: the gateway enforces - // its own list and answers HTTP 400 when a request uses a capability the model does not - // advertise here (`reasoning_effort` on a model without `reasoning`), no matter what the - // underlying model can do elsewhere. Reading them live means a capability the host adds later - // is picked up by the next sync with no code change. - const reasoning = model.capabilities.includes("reasoning"); - const reasoningOptions = reasoning ? neosantaraReasoningControls(model) : undefined; - const inputCost = effectiveUsd(model.pricing.prompt, model); - const outputCost = effectiveUsd(model.pricing.completion, model); + const map = mapping(model); + const reasoning = map.reasoning === true; + const inputCost = price(model.pricing.prompt); + const outputCost = price(model.pricing.completion); if (inputCost === undefined || outputCost === undefined) { throw new Error(`Missing token pricing for Neosantara model '${model.id}'`); } - const cost = { - input: inputCost, - output: outputCost, - cache_read: effectiveUsd(model.pricing.cache_read, model), - cache_write: effectiveUsd(model.pricing.cache_write, model), - }; return factorBaseModel( baseModel, { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoningOptions, - // Reasoning is streamed back in `reasoning_content`, whichever lab built the model. + reasoning_options: reasoning ? neosantaraReasoningControls(model) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, - attachment: model.capabilities.includes("vision"), - tool_call: model.capabilities.includes("function_calling"), - structured_output: model.capabilities.includes("json_mode"), + attachment: map.vision === true, + tool_call: map.tools === true, + structured_output: model.structured_outputs === true, modalities: { - input: neosantaraInputModalities(model.capabilities), - output: ["text"], + input: modalities(model.architecture.input_modalities), + output: modalities(model.architecture.output_modalities), }, status: model.deprecated ? "deprecated" : existing?.status, limit, - cost, + cost: { + input: inputCost, + output: outputCost, + reasoning: reasoning ? nonZeroPrice(model.pricing.internal_reasoning) : undefined, + cache_read: nonZeroPrice(model.pricing.input_cache_read), + cache_write: nonZeroPrice(model.pricing.input_cache_write), + }, }, limit, - existing?.base_model === baseModel ? existing.base_model_omit : undefined, + baseModelOmit, ); } @@ -348,31 +226,26 @@ export const neosantara = { authoritativeHeaders: true, sourceID(model) { if (model.deprecated) return undefined; - // Report anything the public filter accepts but translateModel skipped, whether the cause is - // a missing canonical entry, unusable upstream pricing, or controls that still need review. return meetsNeosantaraPublicFilter(model) ? model.id : undefined; }, skippedNotice(ids) { if (ids.length === 0) return []; return [ - `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit, because the Neosantara catalog reported no usable token pricing, or because their reasoning controls still need review against this host's request schema.`, + `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit or the catalog reported no usable token pricing.`, `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, - "Add a `models//.toml` entry (and, for reasoning models, a reviewed control set) to include them in the next sync.", + "Add a `models//.toml` entry (or an alias in BASE_MODEL_ALIASES) to include them in the next sync.", ]; }, async fetchModels() { - const [models, pricing] = await Promise.all([ - fetchJson(MODELS_ENDPOINT), - fetchJson(PRICING_ENDPOINT), - ]); - return { models, pricing }; + return fetchJson(CATALOG_ENDPOINT); }, parseModels(raw) { - const combined = NeosantaraCombinedResponse.parse(raw); - return mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(combined.models), - NeosantaraPricingResponse.parse(combined.pricing), - ); + const data = NeosantaraCatalogResponse.parse(raw).data; + // An empty catalog would delete every model file; fail loudly instead of wiping the provider. + if (data.length === 0) { + throw new Error("Neosantara catalog returned no models"); + } + return data; }, translateModel(model, context) { if (!shouldSyncNeosantaraModel(model)) return undefined; @@ -380,7 +253,7 @@ export const neosantara = { return { id: model.id, model: translated, - header: neosantaraModelHeader(model, translated.reasoning_options), + header: neosantaraReasoningHeader(translated.reasoning_options), }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index f8409690c77..28aae51be8b 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -2,560 +2,260 @@ import { expect, test } from "bun:test"; import { buildNeosantaraModel, - mergeNeosantaraCatalogs, + meetsNeosantaraPublicFilter, neosantara, - neosantaraFxHeader, - neosantaraInputModalities, neosantaraReasoningControls, neosantaraReasoningHeader, - NeosantaraModelsResponse, - NeosantaraPricingResponse, + NeosantaraCatalogResponse, resolveNeosantaraBaseModel, shouldSyncNeosantaraModel, } from "../src/sync/providers/neosantara.js"; -const modelsResponse = { - object: "list", +// A /v1/catalog response in the models.dev / LLM Gateway shape the backend now emits. +const catalogResponse = { data: [ { id: "gemini-3.7-flash", - object: "model", + name: "gemini-3.7-flash", + display_name: "Neosantara: Gemini 3.7 Flash", created: 1_700_000_000, - owned_by: "Neosantara", description: "Fast reasoning model", - context_window: 1_000_000, - max_output_tokens: 8_192, + family: "google", + architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] }, + context_length: 1_000_000, pricing: { - currency: "USD", - prompt: 1.5, - completion: 7.5, - cache_read: 0.15, - cache_write: 1.5, + prompt: "0.000001500000", + completion: "0.000006000000", + internal_reasoning: "0", + input_cache_read: "0.000000150000", + input_cache_write: "0.000001500000", }, - discount: 50, - capabilities: ["text_generation", "function_calling", "json_mode", "vision", "reasoning"], + supported_parameters: ["temperature", "tools", "tool_choice", "response_format", "reasoning"], + structured_outputs: true, + providers: [ + { + providerId: "neosantara", + vision: true, + tools: true, + reasoning: true, + reasoning_efforts: ["none", "low", "medium", "high"], + }, + ], deprecated: false, - deprecation_alternatives: [], }, { id: "gpt-oss-20b", - object: "model", + name: "gpt-oss-20b", + display_name: "Neosantara: Gpt Oss 20b", created: 1_700_000_000, - owned_by: "Neosantara", - description: "IDR-priced open model", - context_window: 131_072, - max_output_tokens: 8_192, - pricing: { currency: "IDR", prompt: 400, completion: 1_600 }, - capabilities: ["text_generation", "function_calling"], + description: "Open model, no reasoning on this host", + family: "openai", + architecture: { input_modalities: ["text"], output_modalities: ["text"] }, + context_length: 131_072, + pricing: { prompt: "0.000000020000", completion: "0.000000080000" }, + supported_parameters: ["temperature", "tools", "tool_choice"], + structured_outputs: true, + providers: [{ providerId: "neosantara", vision: false, tools: true, reasoning: false }], + deprecated: false, + }, + { + id: "glm-4.7-flash", + name: "glm-4.7-flash", + display_name: "Neosantara: Glm 4.7 Flash", + created: 1_700_000_000, + description: "Free reasoning model", + family: "zhipuai", + architecture: { input_modalities: ["text"], output_modalities: ["text"] }, + context_length: 1_000_000, + pricing: { prompt: "0.000000000000", completion: "0.000000000000" }, + supported_parameters: ["temperature", "tools", "reasoning"], + structured_outputs: false, + providers: [{ providerId: "neosantara", vision: false, tools: true, reasoning: true, reasoning_efforts: ["none"] }], deprecated: false, - deprecation_alternatives: [], }, { id: "too-small", - object: "model", + name: "too-small", + display_name: "Neosantara: Too Small", created: 1_700_000_000, - owned_by: "Neosantara", - description: "Too small", - context_window: 32_000, - max_output_tokens: 4_096, - pricing: { currency: "USD", prompt: 1, completion: 2 }, - capabilities: ["function_calling"], + description: "Below the context floor", + family: "openai", + architecture: { input_modalities: ["text"], output_modalities: ["text"] }, + context_length: 32_000, + pricing: { prompt: "0.000001000000", completion: "0.000002000000" }, + supported_parameters: ["tools"], + structured_outputs: false, + providers: [{ providerId: "neosantara", vision: false, tools: true, reasoning: false }], deprecated: false, - deprecation_alternatives: [], }, { id: "no-tools", - object: "model", + name: "no-tools", + display_name: "Neosantara: No Tools", created: 1_700_000_000, - owned_by: "Neosantara", - description: "No tools", - context_window: 128_000, - max_output_tokens: 4_096, - pricing: { currency: "USD", prompt: 1, completion: 2 }, - capabilities: ["text_generation"], + description: "No function calling", + family: "openai", + architecture: { input_modalities: ["text"], output_modalities: ["text"] }, + context_length: 128_000, + pricing: { prompt: "0.000001000000", completion: "0.000002000000" }, + supported_parameters: [], + structured_outputs: false, + providers: [{ providerId: "neosantara", vision: false, tools: false, reasoning: false }], deprecated: false, - deprecation_alternatives: [], }, ], }; -const pricingResponse = { - data: modelsResponse.data.map((model) => ({ - name: model.id, - provider: "Neosantara", - category: "standard", - type: "text", - input: "-", - output: "-", - capabilities: model.capabilities, - context_window: model.context_window, - description: model.description, - raw_pricing: { - currency: model.pricing.currency, - per_million_tokens: { - input: model.pricing.prompt, - output: model.pricing.completion, - cache_read: "cache_read" in model.pricing ? model.pricing.cache_read : undefined, - cache_write: "cache_write" in model.pricing ? model.pricing.cache_write : undefined, - }, - }, - })), - meta: { - count: modelsResponse.data.length, - updated_at: "2026-09-02T16:32:29.553Z", - exchange_rate: { usd_idr: 20_000, currency: "IDR", source: "test" }, - }, -}; +test("parses the single /v1/catalog endpoint", () => { + const parsed = NeosantaraCatalogResponse.parse(catalogResponse); + expect(parsed.data).toHaveLength(5); + expect(parsed.data[0]?.providers[0]?.reasoning_efforts).toEqual(["none", "low", "medium", "high"]); +}); -test("parses and merges both public Neosantara endpoints", () => { - const models = NeosantaraModelsResponse.parse(modelsResponse); - const pricing = NeosantaraPricingResponse.parse(pricingResponse); - const merged = mergeNeosantaraCatalogs(models, pricing); +test("filters to 100k+ context, function calling, image models, and known base models", () => { + const [gemini, oss, glm, tooSmall, noTools] = NeosantaraCatalogResponse.parse(catalogResponse).data; - expect(merged).toHaveLength(4); - expect(merged[0]?.exchange_rate).toBe(20_000); - expect(merged[0]?.exchange_rate_source).toBe("test"); - expect(merged[0]?.exchange_rate_updated_at).toBe("2026-09-02T16:32:29.553Z"); - expect(merged[0]?.pricing.prompt).toBe(1.5); -}); + expect(meetsNeosantaraPublicFilter(gemini!)).toBe(true); + expect(meetsNeosantaraPublicFilter(oss!)).toBe(true); + expect(meetsNeosantaraPublicFilter(glm!)).toBe(true); + expect(meetsNeosantaraPublicFilter(tooSmall!)).toBe(false); // < 100k context + expect(meetsNeosantaraPublicFilter(noTools!)).toBe(false); // no function calling -test("filters to 100k+ context, function calling, and known canonical base models", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - ); + expect(shouldSyncNeosantaraModel(gemini!)).toBe(true); + expect(shouldSyncNeosantaraModel(tooSmall!)).toBe(false); + expect(shouldSyncNeosantaraModel(noTools!)).toBe(false); - expect(shouldSyncNeosantaraModel(merged[0]!)).toBe(true); - expect(shouldSyncNeosantaraModel(merged[2]!)).toBe(false); - expect(shouldSyncNeosantaraModel(merged[3]!)).toBe(false); expect(resolveNeosantaraBaseModel("gemini-3.7-flash")).toBe("google/gemini-3.7-flash"); - expect(resolveNeosantaraBaseModel("unknown-model")).toBeUndefined(); + expect(resolveNeosantaraBaseModel("claude-4.5-sonnet")).toBe("anthropic/claude-sonnet-4-5"); + expect(resolveNeosantaraBaseModel("grok-code-fast")).toBe("xai/grok-4.3"); + expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); }); -test("builds override-only models with effective USD pricing and host reasoning controls", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - ); +test("builds override-only models with per-million USD cost and host reasoning controls", () => { + const [gemini] = NeosantaraCatalogResponse.parse(catalogResponse).data; + const built = buildNeosantaraModel(gemini!, undefined); - const gemini = buildNeosantaraModel(merged[0]!, undefined); - expect(gemini).toMatchObject({ + expect(built).toMatchObject({ base_model: "google/gemini-3.7-flash", - // Uniform host effort enum — the catalog exposes reasoning capability, not grades. - reasoning_options: [ - { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, - ], - // This host streams reasoning in `reasoning_content`. + // Efforts advertised by the catalog are honored (intersected with the host enum). + reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }], interleaved: { field: "reasoning_content" }, limit: { context: 1_000_000 }, - cost: { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 }, + cost: { input: 1.5, output: 6, cache_read: 0.15, cache_write: 1.5 }, }); + // internal_reasoning "0" is unknown, not a published zero. + expect((built as { cost?: { reasoning?: number } }).cost?.reasoning).toBeUndefined(); + // The gateway omits max_output; never author a model output limit from a runtime cap. + expect((built as { limit?: { output?: number } }).limit?.output).toBeUndefined(); +}); - // The host's own capability list decides whether reasoning is served here: it rejects - // `reasoning_effort` with HTTP 400 for a model that does not advertise `reasoning`, however - // the lab entry describes the underlying model. - const idr = buildNeosantaraModel(merged[1]!, undefined); - expect(idr).toMatchObject({ - base_model: "openai/gpt-oss-20b", - reasoning: false, - cost: { input: 0.02, output: 0.08 }, - }); - expect(idr.reasoning_options).toBeUndefined(); - expect(idr.interleaved).toBeUndefined(); +test("keeps a free model's zero token price instead of skipping it", () => { + const glm = NeosantaraCatalogResponse.parse(catalogResponse).data.find((m) => m.id === "glm-4.7-flash")!; + expect(shouldSyncNeosantaraModel(glm)).toBe(true); + + const built = buildNeosantaraModel(glm, undefined); + expect(built.cost).toMatchObject({ input: 0, output: 0 }); + // reasoning matches the base (factored out), but its effort controls are still authored. + expect(built.reasoning_options).toBeDefined(); }); -test("writes FX provenance only for IDR-derived USD prices", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - ); - const usd = merged[0]!; - const idr = merged[1]!; +test("a non-reasoning model carries no reasoning controls, interleaved, or note", () => { + const oss = NeosantaraCatalogResponse.parse(catalogResponse).data.find((m) => m.id === "gpt-oss-20b")!; + const built = buildNeosantaraModel(oss, undefined); - expect(neosantaraFxHeader(usd)).toBeUndefined(); - expect(neosantaraFxHeader(idr)).toBe( - "# FX: IDR prices converted to USD at 1 USD = 20000 IDR (test, 2026-09-02).\n", - ); + expect(built.reasoning).toBe(false); + expect(built.reasoning_options).toBeUndefined(); + expect(built.interleaved).toBeUndefined(); - const translated = neosantara.translateModel(idr, { + const translated = neosantara.translateModel(oss, { existing: () => undefined, authored: () => undefined, }); - expect(translated?.header).toBe(neosantaraFxHeader(idr)); + expect(translated?.header ?? "").not.toContain("reasoning_effort"); }); -test("never advertises a control this host cannot send", () => { - const source = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!; - - // Whatever the upstream lab, the host maps effort onto the same accepted enum: only effort, - // no budget_tokens or toggle, and every value inside the host enum. - for (const id of ["gpt-5.6-terra", "claude-sonnet-5", "glm-5.3-flash", "gemini-3.7-flash"]) { - const options = neosantaraReasoningControls({ ...source, id }); - expect(options.some((option) => option.type === "budget_tokens")).toBe(false); - expect(options.some((option) => option.type === "toggle")).toBe(false); - expect(neosantaraReasoningHeader(options)).toContain("reasoning_effort"); - for (const value of options.flatMap((o) => ("values" in o ? o.values : []))) { - expect(["none", "minimal", "low", "medium", "high", "xhigh"]).toContain(value); - } - } - - // The full host enum is what a reasoning model advertises, including the `none` off value. - const built = buildNeosantaraModel({ ...source, id: "gpt-5.6-terra" }, undefined); - expect(built.reasoning_options).toEqual([ - { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, - ]); +test("maps catalog efforts to toggle / effort / always-on (llmgateway convention)", () => { + // Graded levels within the host enum (max included) -> effort, verbatim. + expect( + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["low", "high", "max"] }] } as never), + ).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); + + // Exactly ["none"] -> on/off toggle. + expect( + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["none"] }] } as never), + ).toEqual([{ type: "toggle" }]); + + // [] -> always-on (reasons, no caller control). + expect( + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: [] }] } as never), + ).toEqual([]); + + // Missing efforts -> also always-on, never a full-enum dump. + expect( + neosantaraReasoningControls({ providers: [{ reasoning: true }] } as never), + ).toEqual([]); + + // Values outside the host enum are dropped. + expect( + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["low", "bogus"] }] } as never), + ).toEqual([{ type: "effort", values: ["low"] }]); }); -test("syncs a reasoning model with no per-model configuration in the module", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - ); +test("toggle models carry a wire-comment header; effort models carry none", () => { + const models = NeosantaraCatalogResponse.parse(catalogResponse).data; + const ctx = { existing: () => undefined, authored: () => undefined }; - const reasoner = { - ...merged[1]!, - capabilities: [...merged[1]!.capabilities, "reasoning"], - }; + // glm-4.7-flash advertises ["none"] -> toggle -> header names the wire field. + const toggle = neosantara.translateModel(models.find((m) => m.id === "glm-4.7-flash")!, ctx); + expect(toggle?.header).toContain("reasoning_effort"); + expect(toggle?.header).toContain("Toggle"); - expect(shouldSyncNeosantaraModel(reasoner)).toBe(true); - expect(buildNeosantaraModel(reasoner, undefined).reasoning_options?.length).toBeGreaterThan(0); -}); - -test("keeps model pricing when the public pricing entry is only partially populated", () => { - const partial = structuredClone(pricingResponse) as typeof pricingResponse; - const entry = partial.data.find((row) => row.name === "gemini-3.7-flash")!; - delete (entry.raw_pricing.per_million_tokens as { output?: number }).output; - - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(partial), - ); - - const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; - expect(gemini.pricing.prompt).toBe(1.5); - expect(gemini.pricing.completion).toBe(7.5); - - expect(buildNeosantaraModel(gemini, undefined).cost).toMatchObject({ - input: 0.75, - output: 3.75, - }); + // gemini-3.7-flash advertises graded effort -> no toggle header. + const effort = neosantara.translateModel(models[0]!, ctx); + expect(effort?.header ?? "").not.toContain("Toggle"); }); -test("derives input modalities from every advertised understanding capability", () => { - expect(neosantaraInputModalities(["text_generation"])).toEqual(["text"]); - expect(neosantaraInputModalities(["text_generation", "vision"])).toEqual(["text", "image"]); - expect(neosantaraInputModalities(["vision", "video_understanding"])).toEqual([ - "text", - "image", - "video", - ]); - - const source = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!; - - // zhipuai/glm-4.6v-flash accepts video upstream, so the host must not narrow the lab entry. - const video = buildNeosantaraModel( - { - ...source, - id: "glm-4.6v-flash", - capabilities: [...source.capabilities, "video_understanding"], - }, - undefined, - ); - expect(video.modalities).toBeUndefined(); -}); - -const imageModelsResponse = { - object: "list", - data: [ - { - id: "gpt-image-2", - object: "model", - created: 1_700_000_000, - owned_by: "Neosantara", - description: "Image generation and editing", - context_window: 0, - max_output_tokens: 0, - pricing: { currency: "USD", per_image: { "1024x1024": 0.04 } }, - capabilities: ["image_generation"], - deprecated: false, - deprecation_alternatives: [], - }, - { - id: "imagen-4.0-fast", - object: "model", - created: 1_700_000_000, - owned_by: "Neosantara", - description: "Image generation without a canonical lab entry", - context_window: 0, - max_output_tokens: 0, - pricing: { currency: "USD", per_image: { "1024x1024": 0.02 } }, - capabilities: ["image_generation"], - deprecated: false, - deprecation_alternatives: [], - }, - ], -}; - -const imagePricingResponse = { - data: imageModelsResponse.data.map((model) => ({ - name: model.id, - provider: "Neosantara", - category: "standard", - type: "image", - input: "-", - output: "-", - capabilities: model.capabilities, - context_window: model.context_window, - description: model.description, - raw_pricing: { currency: "USD", per_image: model.pricing.per_image }, - })), - meta: { - count: imageModelsResponse.data.length, - updated_at: "2026-09-02T16:32:29.553Z", - exchange_rate: { usd_idr: 16_000, currency: "IDR", source: "test" }, - }, -}; - -test("syncs image generation models on per-image pricing without token costs", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(imageModelsResponse), - NeosantaraPricingResponse.parse(imagePricingResponse), - ); - - const image = merged[0]!; - expect(shouldSyncNeosantaraModel(image)).toBe(true); +test("syncs image-generation models on per-image pricing without token cost", () => { + const image = { + id: "gpt-image-2", + name: "gpt-image-2", + display_name: "Neosantara: Gpt Image 2", + created: 1_700_000_000, + description: "Image generation", + family: "openai", + architecture: { input_modalities: ["text"], output_modalities: ["image"] }, + context_length: 0, + pricing: {}, + supported_parameters: [], + structured_outputs: false, + providers: [{ providerId: "neosantara", vision: false, tools: false, reasoning: false }], + deprecated: false, + }; + const parsed = NeosantaraCatalogResponse.parse({ data: [image] }).data[0]!; - const built = buildNeosantaraModel(image, undefined); + expect(shouldSyncNeosantaraModel(parsed)).toBe(true); + const built = buildNeosantaraModel(parsed, undefined); expect("base_model" in built && built.base_model).toBe("openai/gpt-image-2"); expect(built.cost).toBeUndefined(); expect(built.reasoning_options).toBeUndefined(); - expect(built.tool_call).toBeUndefined(); -}); - -test("reports image models that still lack a canonical lab entry", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(imageModelsResponse), - NeosantaraPricingResponse.parse(imagePricingResponse), - ); - - const context = { existing: () => undefined, authored: () => undefined }; - - // The mapped model syncs, so it is never handed to the skip reporter. - expect(neosantara.translateModel(merged[0]!, context)?.id).toBe("gpt-image-2"); - - expect(shouldSyncNeosantaraModel(merged[1]!)).toBe(false); - expect(neosantara.translateModel(merged[1]!, context)).toBeUndefined(); - expect(neosantara.sourceID(merged[1]!)).toBe("imagen-4.0-fast"); -}); - -test("preserves model cache pricing when the public entry omits it in the same currency", () => { - const partial = structuredClone(pricingResponse) as typeof pricingResponse; - const entry = partial.data.find((row) => row.name === "gemini-3.7-flash")!; - const tokens = entry.raw_pricing.per_million_tokens as { - cache_read?: number; - cache_write?: number; - }; - delete tokens.cache_read; - delete tokens.cache_write; - - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(partial), - ); - - const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; - expect(gemini.pricing.cache_read).toBe(0.15); - expect(gemini.pricing.cache_write).toBe(1.5); -}); - -test("never carries cache pricing across a currency change between the two endpoints", () => { - const crossCurrency = structuredClone(pricingResponse) as typeof pricingResponse; - const entry = crossCurrency.data.find((row) => row.name === "gemini-3.7-flash")!; - entry.raw_pricing.currency = "IDR"; - const tokens = entry.raw_pricing.per_million_tokens as { - input?: number; - output?: number; - cache_read?: number; - cache_write?: number; - }; - tokens.input = 24_000; - tokens.output = 120_000; - delete tokens.cache_read; - delete tokens.cache_write; - - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(crossCurrency), - ); - - const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; - expect(gemini.pricing.currency).toBe("IDR"); - expect(gemini.pricing.cache_read).toBeUndefined(); - expect(gemini.pricing.cache_write).toBeUndefined(); -}); - -test("tolerates public pricing entries that omit fields the sync never reads", () => { - const sparse = { - data: [ - { name: "gemini-3.7-flash", raw_pricing: { per_image: 0, currency: "USD" } }, - ...pricingResponse.data.filter((row) => row.name !== "gemini-3.7-flash"), - ], - meta: pricingResponse.meta, - }; - - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(sparse), - ); - - const gemini = merged.find((model) => model.id === "gemini-3.7-flash")!; - expect(gemini.pricing.prompt).toBe(1.5); - expect(gemini.pricing.completion).toBe(7.5); + // No limit.context = 0 authored for a context-less image model. + expect((built as { limit?: { context?: number } }).limit?.context).toBeUndefined(); }); -test("resolves base models from the canonical metadata tree, aliasing only ambiguous ids", () => { - // No alias entry needed: the canonical filename is unique under models/. - expect(resolveNeosantaraBaseModel("gpt-5.6-terra")).toBe("openai/gpt-5.6-terra"); - expect(resolveNeosantaraBaseModel("kimi-k2.5")).toBe("moonshotai/kimi-k2.5"); - expect(resolveNeosantaraBaseModel("gpt-image-2")).toBe("openai/gpt-image-2"); +test("filters deprecated models and reports unmapped ids as skipped", () => { + const deprecated = { ...catalogResponse.data[0]!, deprecated: true }; + expect(shouldSyncNeosantaraModel(NeosantaraCatalogResponse.parse({ data: [deprecated] }).data[0]!)).toBe(false); - // Aliases cover ids the tree cannot resolve on its own. - expect(resolveNeosantaraBaseModel("grok-code-fast")).toBe("xai/grok-4.3"); - expect(resolveNeosantaraBaseModel("claude-opus-7")).toBe("anthropic/claude-opus-4-7"); - expect(resolveNeosantaraBaseModel("qwen3-235b-wse")).toBe( - "alibaba/qwen3-235b-a22b-instruct-2507", - ); + const unmapped = { ...catalogResponse.data[0]!, id: "not-in-the-canonical-tree" }; + const parsed = NeosantaraCatalogResponse.parse({ data: [unmapped] }).data[0]!; + expect(shouldSyncNeosantaraModel(parsed)).toBe(false); + expect(neosantara.sourceID(parsed)).toBe("not-in-the-canonical-tree"); - expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); -}); - -test("reports models skipped for want of a canonical entry", () => { - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - ); - - const unknown = { ...merged[1]!, id: "not-in-the-canonical-tree" }; - - expect(shouldSyncNeosantaraModel(unknown)).toBe(false); - expect(neosantara.sourceID(unknown)).toBe("not-in-the-canonical-tree"); -}); - -test("surfaces skipped models in the sync notice so they are not collected and discarded", () => { + const notice = neosantara.skippedNotice(["not-in-the-canonical-tree"]); + expect(notice.join("\n")).toContain("not-in-the-canonical-tree"); expect(neosantara.skippedNotice([])).toEqual([]); - - const notice = neosantara.skippedNotice(["gemini-3.1-pro", "imagen-4.0-fast"]); - expect(notice.length).toBeGreaterThan(0); - expect(notice.join("\n")).toContain("gemini-3.1-pro"); - expect(notice.join("\n")).toContain("imagen-4.0-fast"); }); -test("skips a model with unusable upstream pricing instead of aborting the whole sync", () => { - const broken = { - object: "list", - data: [ - { - ...modelsResponse.data[0]!, - id: "kimi-k2.5", - pricing: { currency: "USD" }, - discount: undefined, - capabilities: ["text_generation", "function_calling"], - }, - ], - }; - - const merged = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(broken), - NeosantaraPricingResponse.parse({ data: [], meta: pricingResponse.meta }), - ); - - const context = { existing: () => undefined, authored: () => undefined }; - expect(() => neosantara.translateModel(merged[0]!, context)).not.toThrow(); - expect(neosantara.translateModel(merged[0]!, context)).toBeUndefined(); - expect(neosantara.sourceID(merged[0]!)).toBe("kimi-k2.5"); -}); - -test("advertises the host effort enum uniformly for every reasoning model", () => { - const source = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!; - const enumEffort = [ - { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, - ]; - - // Anthropic (lab uses budgets), OpenAI (native effort), and relayed DeepSeek (lab toggle) all - // resolve to the same mapped surface — the module reads capability, not lab shape. - for (const id of ["claude-fable-5", "gpt-5.6-terra", "deepseek-v4-pro", "deepseek-v4-flash"]) { - expect(neosantaraReasoningControls({ ...source, id })).toEqual(enumEffort); - } -}); - -test("honors catalog-published effort levels when they appear, filtered to the host enum", () => { - const source = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!; - - // Future catalog: a per-model effort list is honored, and values the host cannot send (`max`) - // are dropped rather than published. - const future = { ...source, reasoning_efforts: ["low", "high", "max"] }; - expect(neosantaraReasoningControls(future)).toEqual([ - { type: "effort", values: ["low", "high"] }, - ]); - - // An empty or all-invalid catalog list falls back to the full host enum, never to `[]`. - expect(neosantaraReasoningControls({ ...source, reasoning_efforts: ["max"] })).toEqual([ - { type: "effort", values: ["none", "minimal", "low", "medium", "high", "xhigh"] }, - ]); -}); - -test("stamps every reasoning TOML with the note explaining the mapped surface", () => { - const source = mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!; - const context = { existing: () => undefined, authored: () => undefined }; - - // A reasoning model (gemini here) carries the note so a reviewer sees why the shape differs - // from the upstream provider's own entry. - const reasoning = neosantara.translateModel(source, context); - expect(reasoning?.header).toContain("Neosantara relays this model"); - expect(reasoning?.header).toContain("reasoning_effort"); - - // A non-reasoning model carries no reasoning note. - const plain = neosantara.translateModel( - mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[1]!, - context, - ); - expect(plain?.header ?? "").not.toContain("Neosantara relays this model"); -}); - -test("filters deprecated models and never treats the gateway runtime cap as an output limit", () => { - const model = { - ...mergeNeosantaraCatalogs( - NeosantaraModelsResponse.parse(modelsResponse), - NeosantaraPricingResponse.parse(pricingResponse), - )[0]!, - deprecated: true, - max_output_tokens: 2_048, - }; - - expect(shouldSyncNeosantaraModel(model)).toBe(false); - - const built = buildNeosantaraModel({ ...model, deprecated: false }, undefined); - expect(built).toMatchObject({ - base_model: "google/gemini-3.7-flash", - limit: { context: 1_000_000 }, - }); - expect((built as { limit?: { output?: number } }).limit?.output).toBeUndefined(); +test("an empty catalog is fatal rather than wiping every model", () => { + expect(() => neosantara.parseModels({ data: [] })).toThrow(); }); diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 9902c74ffd3..439a3c0962c 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-fable-5" attachment = false structured_output = true @@ -13,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index b2340733b61..7e847b8f6e2 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-4-6" attachment = false structured_output = true @@ -13,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml new file mode 100644 index 00000000000..2816214cf1b --- /dev/null +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -0,0 +1,21 @@ +base_model = "anthropic/claude-opus-4-7" +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "max"] + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml new file mode 100644 index 00000000000..30d50d90c12 --- /dev/null +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -0,0 +1,21 @@ +base_model = "anthropic/claude-opus-4-8" +structured_output = true + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "max"] + +[cost] +input = 5 +output = 25 +cache_read = 0.5 +cache_write = 6.25 + +[limit] +context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index d6a62587cd4..7572f846168 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-opus-5" structured_output = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-7.toml b/providers/neosantara/models/claude-opus-7.toml deleted file mode 100644 index 0f5cf1f3d1f..00000000000 --- a/providers/neosantara/models/claude-opus-7.toml +++ /dev/null @@ -1,27 +0,0 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz -base_model = "anthropic/claude-opus-4-7" -structured_output = true - -[interleaved] -field = "reasoning_content" - -[[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] - -[cost] -input = 5 -output = 25 -cache_read = 0.5 -cache_write = 6.25 - -[limit] -context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-8.toml b/providers/neosantara/models/claude-opus-8.toml deleted file mode 100644 index 20e671e3aa8..00000000000 --- a/providers/neosantara/models/claude-opus-8.toml +++ /dev/null @@ -1,27 +0,0 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz -base_model = "anthropic/claude-opus-4-8" -structured_output = true - -[interleaved] -field = "reasoning_content" - -[[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] - -[cost] -input = 5 -output = 25 -cache_read = 0.5 -cache_write = 6.25 - -[limit] -context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index bb9b094d099..55697bcdc05 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-sonnet-4-6" attachment = false structured_output = true @@ -13,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index e0f816fddaf..da305b074df 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "anthropic/claude-sonnet-5" structured_output = true @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index d464b1dd8ab..86fbc61e565 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false @@ -13,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index 8a827ad198d..e88728c0359 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro-0813" [interleaved] @@ -11,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "high", "max"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 315beb4f259..5876a72228b 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false @@ -13,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "high", "max"] [cost] input = 2.4 diff --git a/providers/neosantara/models/gemini-3.5-flash.toml b/providers/neosantara/models/gemini-3.5-flash.toml index 361a3881539..e0b1ca5643f 100644 --- a/providers/neosantara/models/gemini-3.5-flash.toml +++ b/providers/neosantara/models/gemini-3.5-flash.toml @@ -1,5 +1,11 @@ base_model = "google/gemini-3.5-flash" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] [cost] input = 1.5 diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 3c47bf89fdb..5a7d0dd6e23 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "google/gemini-3.6-flash" [interleaved] @@ -11,13 +5,13 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high"] [cost] -input = 0.75 -output = 3.75 -cache_read = 0.075 -cache_write = 0.75 +input = 1.5 +output = 7.5 +cache_read = 0.15 +cache_write = 1.5 [limit] context = 1_000_000 diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 3322ced4ce0..6f8e88e70aa 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "google/gemini-3.7-flash" [interleaved] @@ -11,13 +5,13 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high"] [cost] -input = 0.75 -output = 3.75 -cache_read = 0.075 -cache_write = 0.75 +input = 1.5 +output = 7.5 +cache_read = 0.15 +cache_write = 1.5 [limit] context = 1_000_000 diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml index b300dd12ab1..a5b609344af 100644 --- a/providers/neosantara/models/glm-4.5-flash.toml +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -1,9 +1,15 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "zhipuai/glm-4.5-flash" attachment = true -reasoning = false structured_output = false +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" + [cost] input = 0.015625 output = 0.046875 diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 548553f8c34..b2507c9f864 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,20 +1,10 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "zhipuai/glm-4.6v-flash" structured_output = false +reasoning_options = [] [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] - [cost] input = 0.015625 output = 0.046875 diff --git a/providers/neosantara/models/glm-4.7-flash.toml b/providers/neosantara/models/glm-4.7-flash.toml index ddb152274c8..177e476c898 100644 --- a/providers/neosantara/models/glm-4.7-flash.toml +++ b/providers/neosantara/models/glm-4.7-flash.toml @@ -1,6 +1,9 @@ base_model = "zhipuai/glm-4.7-flash" -reasoning = false structured_output = false +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0 diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml index e0094620bac..2ee2012297f 100644 --- a/providers/neosantara/models/glm-4.7.toml +++ b/providers/neosantara/models/glm-4.7.toml @@ -1,4 +1,3 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "zhipuai/glm-4.7" attachment = true reasoning = false diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml index bd43fadb0fd..c3de88decf7 100644 --- a/providers/neosantara/models/glm-5.3-flash.toml +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "zhipuai/glm-5.3-flash" attachment = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["low", "high", "max"] [cost] input = 0.15 diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index 34b23c57090..7a4c60e8eda 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -1,10 +1,3 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5-nano" base_model_omit = ["limit.input"] diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index 3362c787fd9..9b9e260aaed 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -1,10 +1,3 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.4-nano" [interleaved] diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 7e16b2cdd19..b0b1d88e76b 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.4" base_model_omit = ["limit.input"] attachment = false diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index 602dec83cf1..2b0e7a4c734 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.5" base_model_omit = ["limit.input"] attachment = false diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml index 5aad56d5558..a48df184dd7 100644 --- a/providers/neosantara/models/gpt-5.6-luna.toml +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-luna" base_model_omit = ["limit.input"] attachment = false diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml index 485b0d09607..9c15d20b07b 100644 --- a/providers/neosantara/models/gpt-5.6-sol.toml +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-sol" base_model_omit = ["limit.input"] attachment = false diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index abe9bbd4a10..ae70ce84908 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "openai/gpt-5.6-terra" base_model_omit = ["limit.input"] structured_output = false diff --git a/providers/neosantara/models/gpt-oss-20b.toml b/providers/neosantara/models/gpt-oss-20b.toml index 2b06c8e0117..89f50478552 100644 --- a/providers/neosantara/models/gpt-oss-20b.toml +++ b/providers/neosantara/models/gpt-oss-20b.toml @@ -1,4 +1,3 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "openai/gpt-oss-20b" reasoning = false diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index bb93a54fcd2..62d92c7bac9 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,19 +1,10 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2-thinking" structured_output = false +reasoning_options = [] [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] - [cost] input = 0.6 output = 2.5 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 0c55b384706..ab689c9f0cd 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.5" structured_output = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 30a6a21d902..2511ad66809 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.6" attachment = false @@ -12,11 +6,11 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] -input = 0.285 -output = 1.2 +input = 0.95 +output = 4 [modalities] input = ["text"] diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 089ceb69cce..257735a36e3 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k3" attachment = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index eda45a05466..5a1893dedfb 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" [interleaved] @@ -11,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 0 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index 5a5738f3b45..2c996b8acf0 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" [interleaved] @@ -11,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index b3a753e2bac..be419122e9d 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 0 diff --git a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml index 98845e4a200..d9c4f8cba27 100644 --- a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml +++ b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml @@ -1,11 +1,10 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). base_model = "nvidia/llama-3.3-nemotron-super-49b-v1.5" reasoning = false structured_output = true [cost] input = 0.285 -output = 1.999438 +output = 1.999437 [limit] context = 132_000 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index 6faf5ab0d02..ad08a6deedf 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,19 +1,10 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "minimax/MiniMax-M2.7" structured_output = true +reasoning_options = [] [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] - [cost] -input = 0.09 -output = 0.36 +input = 0.3 +output = 1.2 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index e5dfe4493c9..08452404849 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -1,9 +1,3 @@ -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "meta/muse-spark-1.1" structured_output = false @@ -12,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 1.25 diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 1486e1838a2..58b1f031381 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,10 +1,3 @@ -# FX: IDR prices converted to USD at 1 USD = 16000 IDR (ExchangeRate-API, 2026-09-03). -# Reasoning: Neosantara relays this model and normalizes reasoning onto a -# single OpenAI-compatible `reasoning_effort` field, mapping the upstream model's native shape -# (Anthropic thinking budget, DeepSeek/GLM toggle, OpenAI effort, …) behind it. The catalog -# advertises only that the model reasons, not its effort levels, so this entry lists the host's -# accepted effort enum rather than the upstream/lab control set. This is Neosantara's request -# surface, not the origin provider's API. https://docs.neosantara.xyz base_model = "stepfun/step-3.5-flash" structured_output = true @@ -13,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] [cost] input = 0.1875 From 10d25e0ca3b1086bc1cff3b8716d5a4b8d8ae3b3 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Thu, 3 Sep 2026 18:27:38 +0000 Subject: [PATCH 16/29] fix(neosantara): per-model reasoning ladders from lab/peer baselines Replace the uniform host-enum reasoning surface with per-model effort lists that follow each model's real controls (first-party lab entry + same-surface OpenRouter/Vercel peers), never a full-enum dump: - Poolside Laguna, InclusionAI Ling, Moonshot Kimi K2.5/K2.6 -> toggle - StepFun Step-3.5 -> low/high; Meta Muse -> minimal..xhigh - GPT-5.4/5.5 -> none/low/medium/high/xhigh (no minimal); GPT-5.6 adds max - Claude -> none/low/medium/high; Kimi K3 -> none/low/high/max - GLM-4.7 / *V and other forced/always-on models -> [] (no caller control) Distinguish an explicit empty reasoning_efforts (always-on, []) from a missing one: a reasoning model whose surface the catalog does not report is now skipped and reported, not stamped always-on. Toggle entries carry the leading wire comment. Rewrite provider.toml for the /v1/catalog source and the []/toggle/effort mapping. Regenerated TOMLs (6 toggle, 4 always-on, 24 effort); re-sync is idempotent and bun validate passes. --- .../core/src/sync/providers/neosantara.ts | 19 +++++++++-- packages/core/test/neosantara-sync.test.ts | 24 ++++++++++--- .../neosantara/models/claude-fable-5.toml | 2 +- .../neosantara/models/claude-opus-4-6.toml | 2 +- .../neosantara/models/claude-opus-4-7.toml | 2 +- .../neosantara/models/claude-opus-4-8.toml | 2 +- .../neosantara/models/claude-opus-5.toml | 2 +- .../neosantara/models/claude-sonnet-4-6.toml | 2 +- .../neosantara/models/claude-sonnet-5.toml | 2 +- .../models/deepseek-v4-flash-0731.toml | 8 ++++- providers/neosantara/models/gpt-5-nano.toml | 2 +- providers/neosantara/models/gpt-5.4-nano.toml | 2 +- providers/neosantara/models/gpt-5.4.toml | 2 +- providers/neosantara/models/gpt-5.5.toml | 2 +- providers/neosantara/models/gpt-5.6-luna.toml | 2 +- providers/neosantara/models/gpt-5.6-sol.toml | 2 +- .../neosantara/models/gpt-5.6-terra.toml | 2 +- providers/neosantara/models/kimi-k2.5.toml | 5 +-- providers/neosantara/models/kimi-k2.6.toml | 5 +-- providers/neosantara/models/kimi-k3.toml | 2 +- providers/neosantara/models/laguna-s-2.1.toml | 5 +-- .../neosantara/models/laguna-xs-2.1.toml | 5 +-- .../neosantara/models/ling-3.0-flash-fin.toml | 5 +-- .../neosantara/models/muse-spark-1.1.toml | 2 +- .../neosantara/models/step-3.5-flash.toml | 2 +- providers/neosantara/provider.toml | 34 +++++++------------ 26 files changed, 87 insertions(+), 57 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 2a74dc25e13..8022a4bb484 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -89,8 +89,15 @@ const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns thinking off; a # value turns it on. https://docs.neosantara.xyz `; +// Whether the catalog reported a caller-control surface for this reasoning model. A missing +// `reasoning_efforts` is "unknown" (skip + report), NOT an affirmative always-on `[]`. +function hasReasoningEfforts(model: NeosantaraSourceModel) { + return Array.isArray(mapping(model).reasoning_efforts); +} + // The catalog reports each model's real effort surface: [] = always-on (no caller control), -// ["none"] = on/off toggle, otherwise the graded levels (intersected with the host enum). +// ["none"] = on/off toggle, otherwise the graded levels (intersected with the host enum). Only +// called once the surface is known (see shouldSyncNeosantaraModel). export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { const efforts = (mapping(model).reasoning_efforts ?? []).filter((effort) => HOST_EFFORT_SET.has(effort), @@ -148,7 +155,13 @@ export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { if (!meetsNeosantaraPublicFilter(model)) return false; if (isImageModel(model)) return true; // Skip rather than throw: one missing price costs a single model, not the run. - return price(model.pricing.prompt) !== undefined && price(model.pricing.completion) !== undefined; + if (price(model.pricing.prompt) === undefined || price(model.pricing.completion) === undefined) { + return false; + } + // A reasoning model with no reported effort surface is unknown, not always-on: skip it (and + // report it) rather than stamping `[]`. Explicit `[]` from the catalog is a real always-on set. + if (mapping(model).reasoning === true && !hasReasoningEfforts(model)) return false; + return true; } export function buildNeosantaraModel( @@ -231,7 +244,7 @@ export const neosantara = { skippedNotice(ids) { if (ids.length === 0) return []; return [ - `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit or the catalog reported no usable token pricing.`, + `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit, the catalog reported no usable token pricing, or a reasoning model did not report its effort surface.`, `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, "Add a `models//.toml` entry (or an alias in BASE_MODEL_ALIASES) to include them in the next sync.", ]; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 28aae51be8b..66f3ce9c90e 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -190,17 +190,31 @@ test("maps catalog efforts to toggle / effort / always-on (llmgateway convention neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: [] }] } as never), ).toEqual([]); - // Missing efforts -> also always-on, never a full-enum dump. - expect( - neosantaraReasoningControls({ providers: [{ reasoning: true }] } as never), - ).toEqual([]); - // Values outside the host enum are dropped. expect( neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["low", "bogus"] }] } as never), ).toEqual([{ type: "effort", values: ["low"] }]); }); +test("skips a reasoning model whose effort surface is unknown (missing != always-on)", () => { + const source = NeosantaraCatalogResponse.parse(catalogResponse).data[0]!; + // Reasoning model with a real surface syncs. + expect(shouldSyncNeosantaraModel(source)).toBe(true); + + // Same model but the catalog omits reasoning_efforts -> unknown -> skipped and reported, + // never stamped as always-on `[]`. + const unknown = { ...source, providers: [{ ...source.providers[0], reasoning_efforts: undefined }] }; + const parsed = NeosantaraCatalogResponse.parse({ data: [unknown] }).data[0]!; + expect(shouldSyncNeosantaraModel(parsed)).toBe(false); + expect(neosantara.sourceID(parsed)).toBe(parsed.id); + + // Explicit [] is different: it is an affirmative always-on set and still syncs. + const alwaysOn = { ...source, providers: [{ ...source.providers[0], reasoning_efforts: [] }] }; + const parsedAlwaysOn = NeosantaraCatalogResponse.parse({ data: [alwaysOn] }).data[0]!; + expect(shouldSyncNeosantaraModel(parsedAlwaysOn)).toBe(true); + expect(buildNeosantaraModel(parsedAlwaysOn, undefined).reasoning_options).toEqual([]); +}); + test("toggle models carry a wire-comment header; effort models carry none", () => { const models = NeosantaraCatalogResponse.parse(catalogResponse).data; const ctx = { existing: () => undefined, authored: () => undefined }; diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 439a3c0962c..41acca93070 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 7e847b8f6e2..124f62052aa 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index 2816214cf1b..08342df77e3 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index 30d50d90c12..b496ab3fe9d 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 7572f846168..56733f35687 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 55697bcdc05..701eebc75d4 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index da305b074df..705807f321b 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["none", "low", "medium", "high"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash-0731.toml b/providers/neosantara/models/deepseek-v4-flash-0731.toml index 7e23df01084..2f294c28af7 100644 --- a/providers/neosantara/models/deepseek-v4-flash-0731.toml +++ b/providers/neosantara/models/deepseek-v4-flash-0731.toml @@ -1,5 +1,11 @@ base_model = "deepseek/deepseek-v4-flash-0731" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index 7a4c60e8eda..e3be6468076 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 0.050625 diff --git a/providers/neosantara/models/gpt-5.4-nano.toml b/providers/neosantara/models/gpt-5.4-nano.toml index 9b9e260aaed..0c7f54843de 100644 --- a/providers/neosantara/models/gpt-5.4-nano.toml +++ b/providers/neosantara/models/gpt-5.4-nano.toml @@ -5,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 0.2025 diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index b0b1d88e76b..67d20995843 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 2.5 diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index 2b0e7a4c734..3b86415307d 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 5 diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml index a48df184dd7..188399c8550 100644 --- a/providers/neosantara/models/gpt-5.6-luna.toml +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -8,7 +8,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 0.2 diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml index 9c15d20b07b..e4e71af98a5 100644 --- a/providers/neosantara/models/gpt-5.6-sol.toml +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -8,7 +8,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index ae70ce84908..a2f93470a5c 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 2 diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index ab689c9f0cd..029978e234c 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,3 +1,5 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.5" structured_output = false @@ -5,8 +7,7 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +type = "toggle" [cost] input = 0.6 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 2511ad66809..d3f01a742fb 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,3 +1,5 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "moonshotai/kimi-k2.6" attachment = false @@ -5,8 +7,7 @@ attachment = false field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +type = "toggle" [cost] input = 0.95 diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 257735a36e3..2ee94c56b5f 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index 5a1893dedfb..dfb8da6d068 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,11 +1,12 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "poolside/laguna-s-2.1" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index 2c996b8acf0..4b08ab88aa6 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,11 +1,12 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "poolside/laguna-xs-2.1" [interleaved] field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index be419122e9d..66458530eed 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,3 +1,5 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false @@ -5,8 +7,7 @@ structured_output = false field = "reasoning_content" [[reasoning_options]] -type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +type = "toggle" [cost] input = 0 diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index 08452404849..c81c2320be0 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +values = ["minimal", "low", "medium", "high", "xhigh"] [cost] input = 1.25 diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 58b1f031381..2c5cfb3a8bf 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] +values = ["low", "high"] [cost] input = 0.1875 diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index e3d062a36d8..24d0e6a718a 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -1,27 +1,19 @@ -# Sources (accessed 2026-09-03): +# Source (accessed 2026-09-03): # - https://docs.neosantara.xyz -# - https://api.neosantara.xyz/v1/models (catalog, public, no key) -# - https://api.neosantara.xyz/v1/public/pricing (raw_pricing + meta.exchange_rate, public) +# - https://api.neosantara.xyz/v1/catalog (models.dev / LLM Gateway shape, public, no key) # -# Reasoning: Neosantara is a relay. It normalizes every model's reasoning onto a single -# OpenAI-compatible `reasoning_effort` field and maps each upstream native shape (Anthropic -# thinking budgets, DeepSeek/GLM toggles, OpenAI effort, …) behind it, so callers never send an -# upstream-native control. Accepted enum (verified live, `max` → HTTP 400): -# reasoning_effort = none|minimal|low|medium|high|xhigh -# The catalog advertises only whether a model reasons, not its effort levels, so every -# reasoning model advertises this full enum rather than a lab- or peer-derived subset; each -# generated reasoning TOML carries a note explaining that divergence. If the catalog later -# publishes per-model effort levels, the sync consumes them automatically. +# The sync consumes the single /v1/catalog endpoint. The gateway does the host-specific work +# upstream (USD-per-token pricing incl. IDR conversion and any discount, capability flags, and +# the reasoning surface), so the sync is a thin, override-only translation. # -# Capability flags (reasoning, vision, json_mode, …) are read from each catalog entry's -# `capabilities` array and never inherited from lab metadata, because this gateway enforces -# its own list: a model the lab ships as a reasoner is published here with reasoning = false -# until the catalog advertises it, and starts publishing controls on the sync after it does. -# A model without the `reasoning` capability rejects `reasoning_effort` with HTTP 400. -# -# Pricing: raw_pricing is per million tokens in USD or IDR and excludes `discount`, -# which is a percentage applied at billing time. Published costs are therefore -# raw * (100 - discount) / 100, with IDR converted at meta.exchange_rate.usd_idr. +# Reasoning: the catalog reports each model's real per-model effort surface in providers[]. +# reasoning_efforts, mapped to reasoning_options as: +# [] -> [] (always-on: reasons, no caller control) +# ["none"] -> toggle (reasoning_effort = "none" is off; carries a leading wire comment) +# graded set -> effort, using the model's real levels (lab + same-surface peers), never a +# full enum. Host ceiling is none|minimal|low|medium|high|xhigh|max. +# A reasoning model that does not report a surface is skipped and reported (its controls are +# unknown, not always-on). Capability flags come from the catalog, not lab metadata. name = "Neosantara" env = ["NEOSANTARA_API_KEY"] npm = "@ai-sdk/openai-compatible" From dee3a54dcf1accfedafba9ddd22f0f2b0aa2d1ef Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 03:39:21 +0000 Subject: [PATCH 17/29] fix(neosantara): derive reasoning effort levels from the first-party lab entry Per-family effort ladders could never match AGENTS.md's per-model baseline (gpt-5-nano != gpt-5.4, claude-opus-4-6 != 4-7, gemini-3.6 has minimal), so the sync now reads each model's first-party lab entry from the canonical tree and uses its effort values, intersected with the host enum. The catalog reasoning_efforts supplies only the host mechanism: [] = always-on, ["none"] = toggle, else graded (with `none` kept only when the lab supports off and the host exposes it). Fixes the flagged ladders: gpt-5-nano -> minimal/low/medium/high; gpt-5.4 -> none/low/medium/high/xhigh; claude-opus-4-6 -> low/medium/high/max (4-7 adds xhigh); gemini-3.6-flash -> minimal/low/medium/high; deepseek-v4-flash -> none/low/high/max (pro none/high/max); step-3.5 -> low/high. No full-enum dumps or invented levels. --- .../core/src/sync/providers/neosantara.ts | 69 ++++++++++++++++--- packages/core/test/neosantara-sync.test.ts | 61 +++++++++++----- .../neosantara/models/claude-fable-5.toml | 2 +- .../neosantara/models/claude-opus-4-6.toml | 2 +- .../neosantara/models/claude-opus-4-7.toml | 2 +- .../neosantara/models/claude-opus-4-8.toml | 2 +- .../neosantara/models/claude-opus-5.toml | 2 +- .../neosantara/models/claude-sonnet-4-6.toml | 2 +- .../neosantara/models/claude-sonnet-5.toml | 2 +- .../neosantara/models/deepseek-v4-flash.toml | 2 +- .../neosantara/models/gemini-3.5-flash.toml | 2 +- .../neosantara/models/gemini-3.6-flash.toml | 2 +- .../neosantara/models/gemini-3.7-flash.toml | 2 +- providers/neosantara/models/gpt-5-nano.toml | 2 +- 14 files changed, 115 insertions(+), 39 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 8022a4bb484..40370a4bb69 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -1,3 +1,6 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; + import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; @@ -10,6 +13,42 @@ const MIN_CONTEXT_WINDOW = 100_000; const HOST_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; const HOST_EFFORT_SET: ReadonlySet = new Set(HOST_EFFORTS); +// Graded effort levels come from each model's first-party lab entry (AGENTS.md baseline), read +// from the canonical tree — never guessed per family. Resolved from the module, not the cwd. +const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers"); +const labReasoningCache = new Map(); + +function firstPartyLabReasoning(baseModel: string): { effort: string[]; toggle: boolean } { + const cached = labReasoningCache.get(baseModel); + if (cached !== undefined) return cached; + const [lab, ...rest] = baseModel.split("/"); + const file = path.join(PROVIDERS_DIR, lab ?? "", "models", `${rest.join("/")}.toml`); + let effort: string[] = []; + let toggle = false; + try { + if (existsSync(file)) { + const toml = Bun.TOML.parse(readFileSync(file, "utf8")) as { reasoning_options?: unknown }; + const options = Array.isArray(toml.reasoning_options) ? toml.reasoning_options : []; + for (const option of options) { + if (typeof option !== "object" || option === null) continue; + const type = (option as { type?: unknown }).type; + if (type === "toggle") toggle = true; + if (type === "effort") { + const values = (option as { values?: unknown }).values; + if (Array.isArray(values)) { + effort = values.filter((value): value is string => typeof value === "string"); + } + } + } + } + } catch { + // Unreadable lab entry: fall back to the host-reported efforts below. + } + const result = { effort, toggle }; + labReasoningCache.set(baseModel, result); + return result; +} + const CatalogProvider = z .object({ providerId: z.string().optional(), @@ -95,16 +134,28 @@ function hasReasoningEfforts(model: NeosantaraSourceModel) { return Array.isArray(mapping(model).reasoning_efforts); } -// The catalog reports each model's real effort surface: [] = always-on (no caller control), -// ["none"] = on/off toggle, otherwise the graded levels (intersected with the host enum). Only -// called once the surface is known (see shouldSyncNeosantaraModel). -export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { - const efforts = (mapping(model).reasoning_efforts ?? []).filter((effort) => +// The catalog reports the host mechanism per model: [] = always-on (no caller control), +// ["none"] = on/off toggle, otherwise the host forwards graded effort. For the graded case the +// levels come from the first-party lab entry (AGENTS.md baseline) intersected with the host +// enum; `none` (off) is kept only when the lab supports off (toggle or `none` in its effort) AND +// the host exposes it. Only called once the surface is known (see shouldSyncNeosantaraModel). +export function neosantaraReasoningControls( + model: NeosantaraSourceModel, + baseModel: string, +): ReasoningControls { + const host = (mapping(model).reasoning_efforts ?? []).filter((effort) => HOST_EFFORT_SET.has(effort), ); - if (efforts.length === 0) return []; - if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; - return [{ type: "effort", values: efforts as never }]; + if (host.length === 0) return []; + if (host.length === 1 && host[0] === "none") return [{ type: "toggle" }]; + + const lab = firstPartyLabReasoning(baseModel); + const labEffort = lab.effort.filter((effort) => HOST_EFFORT_SET.has(effort)); + const graded = labEffort.filter((effort) => effort !== "none"); + // No graded levels to borrow from the lab (toggle/budget-only): use the host's accepted set. + if (graded.length === 0) return [{ type: "effort", values: host as never }]; + const includeNone = (lab.toggle || labEffort.includes("none")) && host.includes("none"); + return [{ type: "effort", values: (includeNone ? ["none", ...graded] : graded) as never }]; } // A toggle needs its wire comment; effort/always-on models carry none. @@ -200,7 +251,7 @@ export function buildNeosantaraModel( { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoning ? neosantaraReasoningControls(model) : undefined, + reasoning_options: reasoning ? neosantaraReasoningControls(model, baseModel) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: map.vision === true, tool_call: map.tools === true, diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 66f3ce9c90e..218b7fb6e26 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -137,8 +137,9 @@ test("builds override-only models with per-million USD cost and host reasoning c expect(built).toMatchObject({ base_model: "google/gemini-3.7-flash", - // Efforts advertised by the catalog are honored (intersected with the host enum). - reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }], + // Levels come from the first-party lab entry (google/gemini-3.7-flash = low/medium/high), + // not the host's advertised list; `none` is dropped because the lab exposes no off. + reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }], interleaved: { field: "reasoning_content" }, limit: { context: 1_000_000 }, cost: { input: 1.5, output: 6, cache_read: 0.15, cache_write: 1.5 }, @@ -174,26 +175,50 @@ test("a non-reasoning model carries no reasoning controls, interleaved, or note" expect(translated?.header ?? "").not.toContain("reasoning_effort"); }); -test("maps catalog efforts to toggle / effort / always-on (llmgateway convention)", () => { - // Graded levels within the host enum (max included) -> effort, verbatim. - expect( - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["low", "high", "max"] }] } as never), - ).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); +test("maps the host mechanism: toggle / always-on / graded", () => { + const ctl = (efforts, base = "example/unknown") => + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: efforts }] } as never, base); // Exactly ["none"] -> on/off toggle. - expect( - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["none"] }] } as never), - ).toEqual([{ type: "toggle" }]); - + expect(ctl(["none"])).toEqual([{ type: "toggle" }]); // [] -> always-on (reasons, no caller control). - expect( - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: [] }] } as never), - ).toEqual([]); + expect(ctl([])).toEqual([]); + // Graded with no first-party lab entry -> fall back to the host's accepted set (enum-filtered). + expect(ctl(["low", "high", "max", "bogus"])).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); +}); + +test("graded ladders follow the first-party lab entry per model (not a family guess)", () => { + const ctl = (efforts, base) => + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: efforts }] } as never, base); - // Values outside the host enum are dropped. - expect( - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: ["low", "bogus"] }] } as never), - ).toEqual([{ type: "effort", values: ["low"] }]); + // gpt-5-nano lab = minimal/low/medium/high (no off) -> host `none` is dropped. + expect(ctl(["none", "low", "medium", "high", "xhigh"], "openai/gpt-5-nano")).toEqual([ + { type: "effort", values: ["minimal", "low", "medium", "high"] }, + ]); + // gpt-5.4 lab = none/low/medium/high/xhigh. + expect(ctl(["none", "low", "medium", "high", "xhigh"], "openai/gpt-5.4")).toEqual([ + { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, + ]); + // claude-opus-4-6 lab = low/medium/high/max (+budget); no off -> keep max, no none. + expect(ctl(["none", "low", "medium", "high"], "anthropic/claude-opus-4-6")).toEqual([ + { type: "effort", values: ["low", "medium", "high", "max"] }, + ]); + // claude-opus-4-7 lab adds xhigh. + expect(ctl(["none", "low", "medium", "high"], "anthropic/claude-opus-4-7")).toEqual([ + { type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }, + ]); + // gemini-3.6-flash lab = minimal/low/medium/high. + expect(ctl(["none", "low", "medium", "high"], "google/gemini-3.6-flash")).toEqual([ + { type: "effort", values: ["minimal", "low", "medium", "high"] }, + ]); + // deepseek-v4-flash lab = toggle + low/high/max; host exposes off -> none + low/high/max. + expect(ctl(["none", "high", "max"], "deepseek/deepseek-v4-flash")).toEqual([ + { type: "effort", values: ["none", "low", "high", "max"] }, + ]); + // deepseek-v4-pro lab = toggle + high/max. + expect(ctl(["none", "high", "max"], "deepseek/deepseek-v4-pro")).toEqual([ + { type: "effort", values: ["none", "high", "max"] }, + ]); }); test("skips a reasoning model whose effort surface is unknown (missing != always-on)", () => { diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 41acca93070..f6b40a54090 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 124f62052aa..56997bc694d 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index 08342df77e3..0b5bb1d2073 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index b496ab3fe9d..576c80c41b2 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 56733f35687..0eb4feacc9c 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 701eebc75d4..2cc63e05962 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index 705807f321b..d8e36005eff 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 86fbc61e565..2e7c0473e49 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/gemini-3.5-flash.toml b/providers/neosantara/models/gemini-3.5-flash.toml index e0b1ca5643f..4b6ba44c0a3 100644 --- a/providers/neosantara/models/gemini-3.5-flash.toml +++ b/providers/neosantara/models/gemini-3.5-flash.toml @@ -5,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["minimal", "low", "medium", "high"] [cost] input = 1.5 diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 5a7d0dd6e23..e7a86e73a3a 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -5,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["minimal", "low", "medium", "high"] [cost] input = 1.5 diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 6f8e88e70aa..06615eafbcf 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -5,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high"] [cost] input = 1.5 diff --git a/providers/neosantara/models/gpt-5-nano.toml b/providers/neosantara/models/gpt-5-nano.toml index e3be6468076..1d8b65e0bc4 100644 --- a/providers/neosantara/models/gpt-5-nano.toml +++ b/providers/neosantara/models/gpt-5-nano.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh"] +values = ["minimal", "low", "medium", "high"] [cost] input = 0.050625 From 23b5103b5420afb3ffe4cc140faac3784e42a1be Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 04:25:04 +0000 Subject: [PATCH 18/29] fix(neosantara): source per-model reasoning surface from the catalog, copy it verbatim The /v1/catalog endpoint now resolves each model's real reasoning_effort surface host-side from that model's models.dev lab entry (with documented host exceptions: DeepSeek collapses low/medium->high so it accepts high/max; z.ai GLM-4.7 and vision variants force thinking on; Anthropic is served via the OpenAI-compatible reasoning_effort field so off = none; dedicated -reasoning variants are always-on). The sync therefore drops the tree-derivation/intersection and copies the catalog's reasoning_efforts verbatim (llmgateway convention): [] -> always-on, ["none"] -> toggle, else effort, intersected with the host enum defensively. This removes the invented levels the reviewer flagged: deepseek-v4-flash is none/high/max (no invented low), gpt-5-nano is minimal/low/medium/high, dated snapshots resolve to the canonical entry (deepseek-v4-flash-0731 -> none/high/max), and every listed reasoning model except the two neosantara-internal garda ids now carries an accurate surface. --- .../core/src/sync/providers/neosantara.ts | 71 +++---------------- packages/core/test/neosantara-sync.test.ts | 51 +++---------- .../neosantara/models/claude-fable-5.toml | 2 +- .../neosantara/models/claude-opus-4-6.toml | 2 +- .../neosantara/models/claude-opus-4-7.toml | 2 +- .../neosantara/models/claude-opus-4-8.toml | 2 +- .../neosantara/models/claude-opus-5.toml | 2 +- .../neosantara/models/claude-sonnet-4-6.toml | 2 +- .../neosantara/models/deepseek-v4-flash.toml | 2 +- 9 files changed, 29 insertions(+), 107 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 40370a4bb69..618cb4f3d1f 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -1,6 +1,4 @@ import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; - import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; @@ -9,46 +7,10 @@ import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js" const CATALOG_ENDPOINT = "https://api.neosantara.xyz/v1/catalog"; const MIN_CONTEXT_WINDOW = 100_000; -// Accepted reasoning_effort enum (up to `max`); efforts from the catalog are intersected with it. +// Accepted reasoning_effort enum; efforts from the catalog are intersected with it defensively. const HOST_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; const HOST_EFFORT_SET: ReadonlySet = new Set(HOST_EFFORTS); -// Graded effort levels come from each model's first-party lab entry (AGENTS.md baseline), read -// from the canonical tree — never guessed per family. Resolved from the module, not the cwd. -const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers"); -const labReasoningCache = new Map(); - -function firstPartyLabReasoning(baseModel: string): { effort: string[]; toggle: boolean } { - const cached = labReasoningCache.get(baseModel); - if (cached !== undefined) return cached; - const [lab, ...rest] = baseModel.split("/"); - const file = path.join(PROVIDERS_DIR, lab ?? "", "models", `${rest.join("/")}.toml`); - let effort: string[] = []; - let toggle = false; - try { - if (existsSync(file)) { - const toml = Bun.TOML.parse(readFileSync(file, "utf8")) as { reasoning_options?: unknown }; - const options = Array.isArray(toml.reasoning_options) ? toml.reasoning_options : []; - for (const option of options) { - if (typeof option !== "object" || option === null) continue; - const type = (option as { type?: unknown }).type; - if (type === "toggle") toggle = true; - if (type === "effort") { - const values = (option as { values?: unknown }).values; - if (Array.isArray(values)) { - effort = values.filter((value): value is string => typeof value === "string"); - } - } - } - } - } catch { - // Unreadable lab entry: fall back to the host-reported efforts below. - } - const result = { effort, toggle }; - labReasoningCache.set(baseModel, result); - return result; -} - const CatalogProvider = z .object({ providerId: z.string().optional(), @@ -134,28 +96,17 @@ function hasReasoningEfforts(model: NeosantaraSourceModel) { return Array.isArray(mapping(model).reasoning_efforts); } -// The catalog reports the host mechanism per model: [] = always-on (no caller control), -// ["none"] = on/off toggle, otherwise the host forwards graded effort. For the graded case the -// levels come from the first-party lab entry (AGENTS.md baseline) intersected with the host -// enum; `none` (off) is kept only when the lab supports off (toggle or `none` in its effort) AND -// the host exposes it. Only called once the surface is known (see shouldSyncNeosantaraModel). -export function neosantaraReasoningControls( - model: NeosantaraSourceModel, - baseModel: string, -): ReasoningControls { - const host = (mapping(model).reasoning_efforts ?? []).filter((effort) => +// The catalog reports each model's real reasoning surface (resolved host-side from the model's +// models.dev lab entry): [] = always-on (no caller control), ["none"] = on/off toggle, otherwise +// the graded effort levels. Copied verbatim (llmgateway convention), intersected with the host +// enum defensively. Only called once the surface is known (see shouldSyncNeosantaraModel). +export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { + const efforts = (mapping(model).reasoning_efforts ?? []).filter((effort) => HOST_EFFORT_SET.has(effort), ); - if (host.length === 0) return []; - if (host.length === 1 && host[0] === "none") return [{ type: "toggle" }]; - - const lab = firstPartyLabReasoning(baseModel); - const labEffort = lab.effort.filter((effort) => HOST_EFFORT_SET.has(effort)); - const graded = labEffort.filter((effort) => effort !== "none"); - // No graded levels to borrow from the lab (toggle/budget-only): use the host's accepted set. - if (graded.length === 0) return [{ type: "effort", values: host as never }]; - const includeNone = (lab.toggle || labEffort.includes("none")) && host.includes("none"); - return [{ type: "effort", values: (includeNone ? ["none", ...graded] : graded) as never }]; + if (efforts.length === 0) return []; + if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; + return [{ type: "effort", values: efforts as never }]; } // A toggle needs its wire comment; effort/always-on models carry none. @@ -251,7 +202,7 @@ export function buildNeosantaraModel( { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoning ? neosantaraReasoningControls(model, baseModel) : undefined, + reasoning_options: reasoning ? neosantaraReasoningControls(model) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: map.vision === true, tool_call: map.tools === true, diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 218b7fb6e26..76f5d3a5200 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -137,9 +137,9 @@ test("builds override-only models with per-million USD cost and host reasoning c expect(built).toMatchObject({ base_model: "google/gemini-3.7-flash", - // Levels come from the first-party lab entry (google/gemini-3.7-flash = low/medium/high), - // not the host's advertised list; `none` is dropped because the lab exposes no off. - reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }], + // The catalog's per-model reasoning_efforts are copied verbatim (host resolves them from the + // model's lab entry), intersected with the host enum. + reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }], interleaved: { field: "reasoning_content" }, limit: { context: 1_000_000 }, cost: { input: 1.5, output: 6, cache_read: 0.15, cache_write: 1.5 }, @@ -175,50 +175,21 @@ test("a non-reasoning model carries no reasoning controls, interleaved, or note" expect(translated?.header ?? "").not.toContain("reasoning_effort"); }); -test("maps the host mechanism: toggle / always-on / graded", () => { - const ctl = (efforts, base = "example/unknown") => - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: efforts }] } as never, base); +test("copies the catalog's per-model reasoning surface verbatim (llmgateway convention)", () => { + const ctl = (efforts) => + neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: efforts }] } as never); // Exactly ["none"] -> on/off toggle. expect(ctl(["none"])).toEqual([{ type: "toggle" }]); // [] -> always-on (reasons, no caller control). expect(ctl([])).toEqual([]); - // Graded with no first-party lab entry -> fall back to the host's accepted set (enum-filtered). - expect(ctl(["low", "high", "max", "bogus"])).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); -}); - -test("graded ladders follow the first-party lab entry per model (not a family guess)", () => { - const ctl = (efforts, base) => - neosantaraReasoningControls({ providers: [{ reasoning: true, reasoning_efforts: efforts }] } as never, base); - - // gpt-5-nano lab = minimal/low/medium/high (no off) -> host `none` is dropped. - expect(ctl(["none", "low", "medium", "high", "xhigh"], "openai/gpt-5-nano")).toEqual([ - { type: "effort", values: ["minimal", "low", "medium", "high"] }, - ]); - // gpt-5.4 lab = none/low/medium/high/xhigh. - expect(ctl(["none", "low", "medium", "high", "xhigh"], "openai/gpt-5.4")).toEqual([ - { type: "effort", values: ["none", "low", "medium", "high", "xhigh"] }, - ]); - // claude-opus-4-6 lab = low/medium/high/max (+budget); no off -> keep max, no none. - expect(ctl(["none", "low", "medium", "high"], "anthropic/claude-opus-4-6")).toEqual([ - { type: "effort", values: ["low", "medium", "high", "max"] }, - ]); - // claude-opus-4-7 lab adds xhigh. - expect(ctl(["none", "low", "medium", "high"], "anthropic/claude-opus-4-7")).toEqual([ - { type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }, - ]); - // gemini-3.6-flash lab = minimal/low/medium/high. - expect(ctl(["none", "low", "medium", "high"], "google/gemini-3.6-flash")).toEqual([ + // Graded levels are copied verbatim (the host resolved them from the model's lab entry), + // intersected with the host enum; out-of-enum values are dropped. + expect(ctl(["minimal", "low", "medium", "high"])).toEqual([ { type: "effort", values: ["minimal", "low", "medium", "high"] }, ]); - // deepseek-v4-flash lab = toggle + low/high/max; host exposes off -> none + low/high/max. - expect(ctl(["none", "high", "max"], "deepseek/deepseek-v4-flash")).toEqual([ - { type: "effort", values: ["none", "low", "high", "max"] }, - ]); - // deepseek-v4-pro lab = toggle + high/max. - expect(ctl(["none", "high", "max"], "deepseek/deepseek-v4-pro")).toEqual([ - { type: "effort", values: ["none", "high", "max"] }, - ]); + expect(ctl(["none", "high", "max"])).toEqual([{ type: "effort", values: ["none", "high", "max"] }]); + expect(ctl(["low", "high", "max", "bogus"])).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); }); test("skips a reasoning model whose effort surface is unknown (missing != always-on)", () => { diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index f6b40a54090..b24ab55bdd2 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 56997bc694d..7e847b8f6e2 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index 0b5bb1d2073..a12915ab67a 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index 576c80c41b2..5c4de5b9134 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 0eb4feacc9c..12c02d4a65f 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 2cc63e05962..55697bcdc05 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 2e7c0473e49..86fbc61e565 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "high", "max"] +values = ["none", "high", "max"] [cost] input = 0.14 From 46560959c7f8d107059001f98f1d5b31737e9b81 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 05:03:45 +0000 Subject: [PATCH 19/29] fix(neosantara): audit reasoning efforts, apply catalog discounts, and clean overrides - Align Claude reasoning models with lab baselines by removing unsupported 'none' effort - Restore 'low' reasoning effort for DeepSeek Flash variants - Remove redundant structured_output overrides from base_model files - Remove unused fs import in sync module - Apply catalog discounts to reflect effective billed rates on Gemini, Kimi, and MiniMax --- packages/core/src/sync/providers/neosantara.ts | 3 +-- providers/neosantara/models/claude-4.5-opus.toml | 1 - providers/neosantara/models/claude-4.5-sonnet.toml | 1 - providers/neosantara/models/claude-fable-5.toml | 3 +-- providers/neosantara/models/claude-opus-4-6.toml | 3 +-- providers/neosantara/models/claude-opus-4-7.toml | 3 +-- providers/neosantara/models/claude-opus-4-8.toml | 3 +-- providers/neosantara/models/claude-opus-5.toml | 2 +- providers/neosantara/models/claude-sonnet-4-6.toml | 3 +-- providers/neosantara/models/claude-sonnet-5.toml | 3 +-- providers/neosantara/models/deepseek-v4-flash-0731.toml | 2 +- providers/neosantara/models/deepseek-v4-flash.toml | 2 +- providers/neosantara/models/gemini-3.6-flash.toml | 8 ++++---- providers/neosantara/models/gemini-3.7-flash.toml | 8 ++++---- providers/neosantara/models/kimi-k2.6.toml | 4 ++-- .../models/llama-3.3-nemotron-super-49b-v1.5.toml | 1 - providers/neosantara/models/minimax-m2.7.toml | 5 ++--- providers/neosantara/models/minimax-m3.toml | 1 - providers/neosantara/models/mistral-large-latest.toml | 1 - providers/neosantara/models/mistral-small-latest.toml | 1 - providers/neosantara/models/step-3.5-flash.toml | 1 - 21 files changed, 22 insertions(+), 37 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 618cb4f3d1f..d66e4372ee1 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -1,4 +1,3 @@ -import { existsSync, readFileSync } from "node:fs"; import { z } from "zod"; import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js"; import { factorBaseModel, resolveModelMetadataBaseModel } from "./openrouter.js"; @@ -206,7 +205,7 @@ export function buildNeosantaraModel( interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: map.vision === true, tool_call: map.tools === true, - structured_output: model.structured_outputs === true, + structured_output: model.structured_outputs === false ? false : undefined, modalities: { input: modalities(model.architecture.input_modalities), output: modalities(model.architecture.output_modalities), diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index e301e38f462..fadc2dde535 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,6 +1,5 @@ base_model = "anthropic/claude-opus-4-5" reasoning = false -structured_output = true [cost] input = 5 diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml index 40aead7e51b..1e586c6eb78 100644 --- a/providers/neosantara/models/claude-4.5-sonnet.toml +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -1,6 +1,5 @@ base_model = "anthropic/claude-sonnet-4-5" reasoning = false -structured_output = true [cost] input = 3 diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index b24ab55bdd2..64cc050068d 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -1,13 +1,12 @@ base_model = "anthropic/claude-fable-5" attachment = false -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh", "max"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 10 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 7e847b8f6e2..9449ea759f3 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,13 +1,12 @@ base_model = "anthropic/claude-opus-4-6" attachment = false -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index a12915ab67a..edf73248854 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -1,12 +1,11 @@ base_model = "anthropic/claude-opus-4-7" -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh", "max"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index 5c4de5b9134..752a1fee6bb 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -1,12 +1,11 @@ base_model = "anthropic/claude-opus-4-8" -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh", "max"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 12c02d4a65f..0eb4feacc9c 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -6,7 +6,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh", "max"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 55697bcdc05..8ff9ce2f17c 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,13 +1,12 @@ base_model = "anthropic/claude-sonnet-4-6" attachment = false -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index d8e36005eff..0dd0b3f5174 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,12 +1,11 @@ base_model = "anthropic/claude-sonnet-5" -structured_output = true [interleaved] field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "xhigh", "max"] +values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash-0731.toml b/providers/neosantara/models/deepseek-v4-flash-0731.toml index 2f294c28af7..41f59d2d4a0 100644 --- a/providers/neosantara/models/deepseek-v4-flash-0731.toml +++ b/providers/neosantara/models/deepseek-v4-flash-0731.toml @@ -5,7 +5,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 86fbc61e565..2e7c0473e49 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -7,7 +7,7 @@ field = "reasoning_content" [[reasoning_options]] type = "effort" -values = ["none", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index e7a86e73a3a..4065d09c643 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -8,10 +8,10 @@ type = "effort" values = ["minimal", "low", "medium", "high"] [cost] -input = 1.5 -output = 7.5 -cache_read = 0.15 -cache_write = 1.5 +input = 0.75 +output = 3.75 +cache_read = 0.075 +cache_write = 0.75 [limit] context = 1_000_000 diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 06615eafbcf..46aa16386a5 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -8,10 +8,10 @@ type = "effort" values = ["low", "medium", "high"] [cost] -input = 1.5 -output = 7.5 -cache_read = 0.15 -cache_write = 1.5 +input = 0.75 +output = 3.75 +cache_read = 0.075 +cache_write = 0.75 [limit] context = 1_000_000 diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index d3f01a742fb..f16e729b890 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -10,8 +10,8 @@ field = "reasoning_content" type = "toggle" [cost] -input = 0.95 -output = 4 +input = 0.285 +output = 1.2 [modalities] input = ["text"] diff --git a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml index d9c4f8cba27..2c8899896e9 100644 --- a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml +++ b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml @@ -1,6 +1,5 @@ base_model = "nvidia/llama-3.3-nemotron-super-49b-v1.5" reasoning = false -structured_output = true [cost] input = 0.285 diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index ad08a6deedf..53be34d1e67 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,10 +1,9 @@ base_model = "minimax/MiniMax-M2.7" -structured_output = true reasoning_options = [] [interleaved] field = "reasoning_content" [cost] -input = 0.3 -output = 1.2 +input = 0.09 +output = 0.36 diff --git a/providers/neosantara/models/minimax-m3.toml b/providers/neosantara/models/minimax-m3.toml index 1e61f2b5e28..c6bffed48e2 100644 --- a/providers/neosantara/models/minimax-m3.toml +++ b/providers/neosantara/models/minimax-m3.toml @@ -1,6 +1,5 @@ base_model = "minimax/MiniMax-M3" reasoning = false -structured_output = true [cost] input = 0.3 diff --git a/providers/neosantara/models/mistral-large-latest.toml b/providers/neosantara/models/mistral-large-latest.toml index 49a84e572d0..dc4f069d5f2 100644 --- a/providers/neosantara/models/mistral-large-latest.toml +++ b/providers/neosantara/models/mistral-large-latest.toml @@ -1,6 +1,5 @@ base_model = "mistral/mistral-large-latest" attachment = false -structured_output = true [cost] input = 0.5 diff --git a/providers/neosantara/models/mistral-small-latest.toml b/providers/neosantara/models/mistral-small-latest.toml index d2ad2f586af..c2f93013a33 100644 --- a/providers/neosantara/models/mistral-small-latest.toml +++ b/providers/neosantara/models/mistral-small-latest.toml @@ -1,7 +1,6 @@ base_model = "mistral/mistral-small-latest" attachment = false reasoning = false -structured_output = true [cost] input = 0.15 diff --git a/providers/neosantara/models/step-3.5-flash.toml b/providers/neosantara/models/step-3.5-flash.toml index 2c5cfb3a8bf..b092cac10ae 100644 --- a/providers/neosantara/models/step-3.5-flash.toml +++ b/providers/neosantara/models/step-3.5-flash.toml @@ -1,5 +1,4 @@ base_model = "stepfun/step-3.5-flash" -structured_output = true [interleaved] field = "reasoning_content" From d055baff629fdf1e025fccf393b98e473bb08bfa Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 05:46:00 +0000 Subject: [PATCH 20/29] fix(neosantara): emit toggle and effort for models with toggleable reasoning - Author toggle + graded effort for Claude Sonnet 5 matching lab and OpenRouter baselines - Include required wire comment naming reasoning_effort = none for toggle models - Support has_toggle in sync provider and test suite for reproducible generation --- .../core/src/sync/providers/neosantara.ts | 27 +++++++++++++++---- packages/core/test/neosantara-sync.test.ts | 10 +++++++ .../neosantara/models/claude-sonnet-5.toml | 5 ++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index d66e4372ee1..10d9cbba671 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -17,6 +17,7 @@ const CatalogProvider = z tools: z.boolean().optional(), reasoning: z.boolean().optional(), reasoning_efforts: z.array(z.string()).optional(), + has_toggle: z.boolean().optional(), }) .passthrough(); @@ -97,14 +98,30 @@ function hasReasoningEfforts(model: NeosantaraSourceModel) { // The catalog reports each model's real reasoning surface (resolved host-side from the model's // models.dev lab entry): [] = always-on (no caller control), ["none"] = on/off toggle, otherwise -// the graded effort levels. Copied verbatim (llmgateway convention), intersected with the host -// enum defensively. Only called once the surface is known (see shouldSyncNeosantaraModel). -export function neosantaraReasoningControls(model: NeosantaraSourceModel): ReasoningControls { - const efforts = (mapping(model).reasoning_efforts ?? []).filter((effort) => +// the graded effort levels. When a model's lab entry exposes toggle + graded effort (e.g. Sonnet 5), +// the host reports `has_toggle: true`, authoring `toggle` alongside the graded effort ladder. +export function neosantaraReasoningControls( + model: NeosantaraSourceModel, + existing?: ExistingModel, +): ReasoningControls { + const map = mapping(model); + const efforts = (map.reasoning_efforts ?? []).filter((effort) => HOST_EFFORT_SET.has(effort), ); if (efforts.length === 0) return []; if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; + + const hasToggle = + map.has_toggle === true || + existing?.reasoning_options?.some((option) => option.type === "toggle"); + + if (hasToggle && !efforts.includes("none")) { + return [ + { type: "toggle" }, + { type: "effort", values: efforts as never }, + ]; + } + return [{ type: "effort", values: efforts as never }]; } @@ -201,7 +218,7 @@ export function buildNeosantaraModel( { name: NAME_OVERRIDES[model.id], reasoning, - reasoning_options: reasoning ? neosantaraReasoningControls(model) : undefined, + reasoning_options: reasoning ? neosantaraReasoningControls(model, existing) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, attachment: map.vision === true, tool_call: map.tools === true, diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 76f5d3a5200..f8172ea2c96 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -190,6 +190,16 @@ test("copies the catalog's per-model reasoning surface verbatim (llmgateway conv ]); expect(ctl(["none", "high", "max"])).toEqual([{ type: "effort", values: ["none", "high", "max"] }]); expect(ctl(["low", "high", "max", "bogus"])).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); + + // When a model has has_toggle: true alongside graded levels, emit toggle + effort. + expect( + neosantaraReasoningControls({ + providers: [{ reasoning: true, reasoning_efforts: ["low", "medium", "high", "xhigh", "max"], has_toggle: true }], + } as never), + ).toEqual([ + { type: "toggle" }, + { type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }, + ]); }); test("skips a reasoning model whose effort surface is unknown (missing != always-on)", () => { diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index 0dd0b3f5174..bdca68e6c7f 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,8 +1,13 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz base_model = "anthropic/claude-sonnet-5" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "xhigh", "max"] From da5fc2ffaaee0d93db058d31c58531254557a515 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 05:52:42 +0000 Subject: [PATCH 21/29] fix(neosantara): point reasoning toggle header to /en/capability/reasoning doc --- packages/core/src/sync/providers/neosantara.ts | 2 +- packages/core/test/neosantara-sync.test.ts | 1 + providers/neosantara/models/claude-sonnet-5.toml | 2 +- providers/neosantara/models/glm-4.5-flash.toml | 2 +- providers/neosantara/models/kimi-k2.5.toml | 2 +- providers/neosantara/models/kimi-k2.6.toml | 2 +- providers/neosantara/models/laguna-s-2.1.toml | 2 +- providers/neosantara/models/laguna-xs-2.1.toml | 2 +- providers/neosantara/models/ling-3.0-flash-fin.toml | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 10d9cbba671..d0f99af0ac6 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -87,7 +87,7 @@ type ReasoningControls = NonNullable; // Toggle controls carry a leading wire comment naming the off value (AGENTS.md requirement). const TOGGLE_HEADER = `# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning `; // Whether the catalog reported a caller-control surface for this reasoning model. A missing diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index f8172ea2c96..701588332a4 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -229,6 +229,7 @@ test("toggle models carry a wire-comment header; effort models carry none", () = const toggle = neosantara.translateModel(models.find((m) => m.id === "glm-4.7-flash")!, ctx); expect(toggle?.header).toContain("reasoning_effort"); expect(toggle?.header).toContain("Toggle"); + expect(toggle?.header).toContain("https://docs.neosantara.xyz/en/capability/reasoning"); // gemini-3.7-flash advertises graded effort -> no toggle header. const effort = neosantara.translateModel(models[0]!, ctx); diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index bdca68e6c7f..5fb4b45d8c0 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-5" [interleaved] diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml index a5b609344af..80c4f82e02d 100644 --- a/providers/neosantara/models/glm-4.5-flash.toml +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.5-flash" attachment = true structured_output = false diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 029978e234c..0b4afeeb15c 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k2.5" structured_output = false diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index f16e729b890..6028ae5d113 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k2.6" attachment = false diff --git a/providers/neosantara/models/laguna-s-2.1.toml b/providers/neosantara/models/laguna-s-2.1.toml index dfb8da6d068..3d9fab5f556 100644 --- a/providers/neosantara/models/laguna-s-2.1.toml +++ b/providers/neosantara/models/laguna-s-2.1.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "poolside/laguna-s-2.1" [interleaved] diff --git a/providers/neosantara/models/laguna-xs-2.1.toml b/providers/neosantara/models/laguna-xs-2.1.toml index 4b08ab88aa6..62eb6836203 100644 --- a/providers/neosantara/models/laguna-xs-2.1.toml +++ b/providers/neosantara/models/laguna-xs-2.1.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "poolside/laguna-xs-2.1" [interleaved] diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 66458530eed..252b2f70bc9 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,5 +1,5 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "inclusionai/ling-3.0-flash-fin" structured_output = false From 22ff07919b9b559d1d5d963ed15ef2af7a0b3d15 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 06:41:24 +0000 Subject: [PATCH 22/29] fix(neosantara): align toggle options with catalog and lab baselines --- .../core/src/sync/providers/neosantara.ts | 20 ++++++++++--------- packages/core/test/neosantara-sync.test.ts | 20 +++++++++++++++++++ providers/neosantara/logo.svg | 2 +- .../models/deepseek-v4-flash-0731.toml | 7 ++++++- .../neosantara/models/deepseek-v4-flash.toml | 7 ++++++- .../models/deepseek-v4-pro-0813.toml | 7 ++++++- .../neosantara/models/deepseek-v4-pro.toml | 7 ++++++- providers/neosantara/models/kimi-k3.toml | 7 ++++++- providers/neosantara/provider.toml | 5 +++++ 9 files changed, 67 insertions(+), 15 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index d0f99af0ac6..3c242c7e966 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -102,7 +102,7 @@ function hasReasoningEfforts(model: NeosantaraSourceModel) { // the host reports `has_toggle: true`, authoring `toggle` alongside the graded effort ladder. export function neosantaraReasoningControls( model: NeosantaraSourceModel, - existing?: ExistingModel, + _existing?: ExistingModel, ): ReasoningControls { const map = mapping(model); const efforts = (map.reasoning_efforts ?? []).filter((effort) => @@ -111,15 +111,17 @@ export function neosantaraReasoningControls( if (efforts.length === 0) return []; if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; - const hasToggle = - map.has_toggle === true || - existing?.reasoning_options?.some((option) => option.type === "toggle"); + const hasToggle = map.has_toggle === true; + const graded = efforts.filter((effort) => effort !== "none"); - if (hasToggle && !efforts.includes("none")) { - return [ - { type: "toggle" }, - { type: "effort", values: efforts as never }, - ]; + if (hasToggle) { + if (graded.length > 0) { + return [ + { type: "toggle" }, + { type: "effort", values: graded as never }, + ]; + } + return [{ type: "toggle" }]; } return [{ type: "effort", values: efforts as never }]; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 701588332a4..46902761bd9 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -200,6 +200,26 @@ test("copies the catalog's per-model reasoning surface verbatim (llmgateway conv { type: "toggle" }, { type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }, ]); + + // When a model has has_toggle: true and efforts contains "none", emit toggle + effort without "none". + expect( + neosantaraReasoningControls({ + providers: [{ reasoning: true, reasoning_efforts: ["none", "low", "high", "max"], has_toggle: true }], + } as never), + ).toEqual([ + { type: "toggle" }, + { type: "effort", values: ["low", "high", "max"] }, + ]); + + // Toggle is driven only by the catalog (has_toggle / ["none"]), not re-inferred from prior TOMLs. + expect( + neosantaraReasoningControls( + { + providers: [{ reasoning: true, reasoning_efforts: ["low", "high", "max"] }], + } as never, + { reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }] } as never, + ), + ).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); }); test("skips a reasoning model whose effort surface is unknown (missing != always-on)", () => { diff --git a/providers/neosantara/logo.svg b/providers/neosantara/logo.svg index 875edca49bb..b03dbb9aac1 100644 --- a/providers/neosantara/logo.svg +++ b/providers/neosantara/logo.svg @@ -1,4 +1,4 @@ - + diff --git a/providers/neosantara/models/deepseek-v4-flash-0731.toml b/providers/neosantara/models/deepseek-v4-flash-0731.toml index 41f59d2d4a0..6b1aa0c13f1 100644 --- a/providers/neosantara/models/deepseek-v4-flash-0731.toml +++ b/providers/neosantara/models/deepseek-v4-flash-0731.toml @@ -1,11 +1,16 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash-0731" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "high", "max"] +values = ["low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 2e7c0473e49..7b83317eb9a 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,3 +1,5 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash" attachment = true structured_output = false @@ -5,9 +7,12 @@ structured_output = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "high", "max"] +values = ["low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index e88728c0359..dce1a846cbd 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,11 +1,16 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro-0813" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "high", "max"] +values = ["high", "max"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index 5876a72228b..cd30ce6f7f8 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,3 +1,5 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro" attachment = true structured_output = false @@ -5,9 +7,12 @@ structured_output = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "high", "max"] +values = ["high", "max"] [cost] input = 2.4 diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 2ee94c56b5f..c3d86a4cb06 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,12 +1,17 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k3" attachment = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "high", "max"] +values = ["low", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index 24d0e6a718a..ed72cfd0d22 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -12,8 +12,13 @@ # ["none"] -> toggle (reasoning_effort = "none" is off; carries a leading wire comment) # graded set -> effort, using the model's real levels (lab + same-surface peers), never a # full enum. Host ceiling is none|minimal|low|medium|high|xhigh|max. +# When has_toggle is true, toggle is authored alongside graded effort (excluding "none"). # A reasoning model that does not report a surface is skipped and reported (its controls are # unknown, not always-on). Capability flags come from the catalog, not lab metadata. +# +# Streaming: Neosantara normalizes reasoning output across all upstream providers into the +# OpenAI-compatible delta.reasoning_content field (see https://docs.neosantara.xyz/en/capability/reasoning). +# All reasoning models therefore advertise interleaved.field = "reasoning_content". name = "Neosantara" env = ["NEOSANTARA_API_KEY"] npm = "@ai-sdk/openai-compatible" From a783c0249dcce314ecc1398135d6e40a69ed8fd8 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 07:14:44 +0000 Subject: [PATCH 23/29] fix(neosantara): emit dynamic base_model and restore reasoning capabilities for lab-aligned models - Delegate base_model and override_name resolution directly to backend /v1/catalog - Remove hardcoded model aliases and override maps from sync module - Restore reasoning effort surfaces for gpt-5.4-mini, gemini-3.1-flash-lite, glm-4.7, claude-4.5-opus, claude-4.5-sonnet, gpt-oss-20b, and gpt-oss-120b - Point deepseek-v4-flash and deepseek-v4-pro to their canonical dated base models (0731 and 0813) --- .../core/src/sync/providers/neosantara.ts | 32 +++++++------------ packages/core/test/neosantara-sync.test.ts | 8 +++-- .../neosantara/models/claude-4.5-opus.toml | 8 ++++- .../neosantara/models/claude-4.5-sonnet.toml | 5 ++- .../neosantara/models/deepseek-v4-flash.toml | 3 +- .../neosantara/models/deepseek-v4-pro.toml | 3 +- .../models/gemini-3.1-flash-lite.toml | 8 ++++- providers/neosantara/models/glm-4.7.toml | 5 ++- providers/neosantara/models/gpt-5.4-mini.toml | 8 ++++- providers/neosantara/models/gpt-oss-120b.toml | 8 ++++- providers/neosantara/models/gpt-oss-20b.toml | 8 ++++- 11 files changed, 64 insertions(+), 32 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 3c242c7e966..efbd629451f 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -48,6 +48,8 @@ export const NeosantaraModel = z pricing: CatalogPricing, supported_parameters: z.array(z.string()).optional(), structured_outputs: z.boolean().optional(), + base_model: z.string().optional(), + override_name: z.string().optional(), providers: z.array(CatalogProvider).min(1), deprecated: z.boolean().optional(), }) @@ -64,25 +66,13 @@ function mapping(model: NeosantaraSourceModel) { return model.providers[0]; } -// Only ids the canonical tree cannot resolve are aliased (renamed generations, coding aliases). -const BASE_MODEL_ALIASES: Record = { - "claude-3-haiku": "anthropic/claude-3-haiku-20240307", - "claude-4.5-opus": "anthropic/claude-opus-4-5", - "claude-4.5-sonnet": "anthropic/claude-sonnet-4-5", - "devstral-2": "mistral/devstral-2512", - "grok-code-fast": "xai/grok-4.3", - "qwen3-235b-wse": "alibaba/qwen3-235b-a22b-instruct-2507", -}; - -export function resolveNeosantaraBaseModel(id: string) { - return BASE_MODEL_ALIASES[id] ?? resolveModelMetadataBaseModel(id); +// Canonical lab base models are supplied directly by the /v1/catalog endpoint, falling +// back to canonical resolution against the local models/ directory. +export function resolveNeosantaraBaseModel(model: NeosantaraSourceModel | string) { + if (typeof model === "string") return resolveModelMetadataBaseModel(model); + return model.base_model ?? resolveModelMetadataBaseModel(model.id); } -// Display names for ids whose canonical entry is a different model (see BASE_MODEL_ALIASES). -const NAME_OVERRIDES: Record = { - "grok-code-fast": "Grok Code Fast", -}; - type ReasoningControls = NonNullable; // Toggle controls carry a leading wire comment naming the off value (AGENTS.md requirement). @@ -171,7 +161,7 @@ export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { } export function shouldSyncNeosantaraModel(model: NeosantaraSourceModel) { - if (model.deprecated || resolveNeosantaraBaseModel(model.id) === undefined) return false; + if (model.deprecated || resolveNeosantaraBaseModel(model) === undefined) return false; if (!meetsNeosantaraPublicFilter(model)) return false; if (isImageModel(model)) return true; // Skip rather than throw: one missing price costs a single model, not the run. @@ -188,7 +178,7 @@ export function buildNeosantaraModel( model: NeosantaraSourceModel, existing: ExistingModel | undefined, ): SyncedModel { - const baseModel = resolveNeosantaraBaseModel(model.id); + const baseModel = resolveNeosantaraBaseModel(model); if (baseModel === undefined) { throw new Error(`No canonical base model mapping for Neosantara model '${model.id}'`); } @@ -218,7 +208,7 @@ export function buildNeosantaraModel( return factorBaseModel( baseModel, { - name: NAME_OVERRIDES[model.id], + name: model.override_name, reasoning, reasoning_options: reasoning ? neosantaraReasoningControls(model, existing) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, @@ -266,7 +256,7 @@ export const neosantara = { return [ `${ids.length} Neosantara models were not created because they lack a canonical \`models/\` entry to inherit, the catalog reported no usable token pricing, or a reasoning model did not report its effort surface.`, `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, - "Add a `models//.toml` entry (or an alias in BASE_MODEL_ALIASES) to include them in the next sync.", + "Ensure the catalog provides a valid base_model or add a `models//.toml` entry to include them in the next sync.", ]; }, async fetchModels() { diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 46902761bd9..7054db53c72 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -126,8 +126,12 @@ test("filters to 100k+ context, function calling, image models, and known base m expect(shouldSyncNeosantaraModel(noTools!)).toBe(false); expect(resolveNeosantaraBaseModel("gemini-3.7-flash")).toBe("google/gemini-3.7-flash"); - expect(resolveNeosantaraBaseModel("claude-4.5-sonnet")).toBe("anthropic/claude-sonnet-4-5"); - expect(resolveNeosantaraBaseModel("grok-code-fast")).toBe("xai/grok-4.3"); + expect( + resolveNeosantaraBaseModel({ id: "claude-4.5-sonnet", base_model: "anthropic/claude-sonnet-4-5" } as never), + ).toBe("anthropic/claude-sonnet-4-5"); + expect( + resolveNeosantaraBaseModel({ id: "grok-code-fast", base_model: "xai/grok-4.3" } as never), + ).toBe("xai/grok-4.3"); expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); }); diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index fadc2dde535..b6da623a758 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,5 +1,11 @@ base_model = "anthropic/claude-opus-4-5" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml index 1e586c6eb78..6efc27bd64f 100644 --- a/providers/neosantara/models/claude-4.5-sonnet.toml +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -1,5 +1,8 @@ base_model = "anthropic/claude-sonnet-4-5" -reasoning = false +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 3 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 7b83317eb9a..66c75266a3e 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,6 +1,7 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning -base_model = "deepseek/deepseek-v4-flash" +base_model = "deepseek/deepseek-v4-flash-0731" +name = "DeepSeek V4 Flash" attachment = true structured_output = false diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index cd30ce6f7f8..df2df4e03ee 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,6 +1,7 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning -base_model = "deepseek/deepseek-v4-pro" +base_model = "deepseek/deepseek-v4-pro-0813" +name = "DeepSeek V4 Pro" attachment = true structured_output = false diff --git a/providers/neosantara/models/gemini-3.1-flash-lite.toml b/providers/neosantara/models/gemini-3.1-flash-lite.toml index a5bd850a1b3..0e87adb20c3 100644 --- a/providers/neosantara/models/gemini-3.1-flash-lite.toml +++ b/providers/neosantara/models/gemini-3.1-flash-lite.toml @@ -1,5 +1,11 @@ base_model = "google/gemini-3.1-flash-lite" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] [cost] input = 0.25 diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml index 2ee2012297f..a1e02119568 100644 --- a/providers/neosantara/models/glm-4.7.toml +++ b/providers/neosantara/models/glm-4.7.toml @@ -1,7 +1,10 @@ base_model = "zhipuai/glm-4.7" attachment = true -reasoning = false structured_output = false +reasoning_options = [] + +[interleaved] +field = "reasoning_content" [cost] input = 0.625 diff --git a/providers/neosantara/models/gpt-5.4-mini.toml b/providers/neosantara/models/gpt-5.4-mini.toml index ae8f9b3b063..c8cfd0370e5 100644 --- a/providers/neosantara/models/gpt-5.4-mini.toml +++ b/providers/neosantara/models/gpt-5.4-mini.toml @@ -1,7 +1,13 @@ base_model = "openai/gpt-5.4-mini" base_model_omit = ["limit.input"] attachment = false -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] [cost] input = 0.75 diff --git a/providers/neosantara/models/gpt-oss-120b.toml b/providers/neosantara/models/gpt-oss-120b.toml index 537d744581e..b1cf0808e38 100644 --- a/providers/neosantara/models/gpt-oss-120b.toml +++ b/providers/neosantara/models/gpt-oss-120b.toml @@ -1,5 +1,11 @@ base_model = "openai/gpt-oss-120b" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 0.15 diff --git a/providers/neosantara/models/gpt-oss-20b.toml b/providers/neosantara/models/gpt-oss-20b.toml index 89f50478552..ffa8cf409b0 100644 --- a/providers/neosantara/models/gpt-oss-20b.toml +++ b/providers/neosantara/models/gpt-oss-20b.toml @@ -1,5 +1,11 @@ base_model = "openai/gpt-oss-20b" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] [cost] input = 0.025 From f2d894d7c3f33e1633534151f5ea5e5cf1d6dfae Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 07:43:29 +0000 Subject: [PATCH 24/29] fix(neosantara): align reasoning surfaces and clean modalities against lab baselines - Expose toggle reasoning controls for Claude 4.5 Sonnet, GLM-4.7, GLM-4.7-Flash, GLM-4.6V-Flash, MiniMax M3, and Nemotron Super 49B - Drop redundant and incorrect vision/attachment overrides on DeepSeek V4 Flash/Pro, GLM-4.7, and GLM-4.5-Flash to inherit text-only lab metadata - Restore reasoning capabilities and effort ladders on MiniMax M3 and Muse Glimmer 30B --- providers/neosantara/models/claude-4.5-opus.toml | 5 +++++ providers/neosantara/models/claude-4.5-sonnet.toml | 6 +++++- providers/neosantara/models/claude-opus-4-6.toml | 5 +++++ providers/neosantara/models/claude-sonnet-4-6.toml | 5 +++++ providers/neosantara/models/deepseek-v4-flash.toml | 4 ---- providers/neosantara/models/deepseek-v4-pro.toml | 4 ---- providers/neosantara/models/glm-4.5-flash.toml | 4 ---- providers/neosantara/models/glm-4.6v-flash.toml | 6 +++++- providers/neosantara/models/glm-4.7-flash.toml | 6 +++++- providers/neosantara/models/glm-4.7.toml | 10 +++++----- .../models/llama-3.3-nemotron-super-49b-v1.5.toml | 9 ++++++++- providers/neosantara/models/minimax-m3.toml | 9 ++++++++- providers/neosantara/models/muse-glimmer-30b.toml | 8 +++++++- 13 files changed, 58 insertions(+), 23 deletions(-) diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index b6da623a758..3dc5d649fe3 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,8 +1,13 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-5" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml index 6efc27bd64f..ff2be1b7756 100644 --- a/providers/neosantara/models/claude-4.5-sonnet.toml +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -1,9 +1,13 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-4-5" -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [cost] input = 3 output = 15 diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index 9449ea759f3..aabe1d6d22c 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,9 +1,14 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-6" attachment = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "max"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 8ff9ce2f17c..03860c5d20d 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,9 +1,14 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-4-6" attachment = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" values = ["low", "medium", "high", "max"] diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 66c75266a3e..3ae3725a6ff 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -2,7 +2,6 @@ # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash-0731" name = "DeepSeek V4 Flash" -attachment = true structured_output = false [interleaved] @@ -19,6 +18,3 @@ values = ["low", "high", "max"] input = 0.14 output = 0.28 cache_read = 0.0028 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index df2df4e03ee..a0a6ec7b6d0 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -2,7 +2,6 @@ # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro-0813" name = "DeepSeek V4 Pro" -attachment = true structured_output = false [interleaved] @@ -19,6 +18,3 @@ values = ["high", "max"] input = 2.4 output = 4.8 cache_read = 0.024 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml index 80c4f82e02d..478818bf433 100644 --- a/providers/neosantara/models/glm-4.5-flash.toml +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.5-flash" -attachment = true structured_output = false [interleaved] @@ -16,6 +15,3 @@ output = 0.046875 [limit] context = 128_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index b2507c9f864..1cab3f986bc 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,10 +1,14 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.6v-flash" structured_output = false -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [cost] input = 0.015625 output = 0.046875 diff --git a/providers/neosantara/models/glm-4.7-flash.toml b/providers/neosantara/models/glm-4.7-flash.toml index 177e476c898..1690bd2ca58 100644 --- a/providers/neosantara/models/glm-4.7-flash.toml +++ b/providers/neosantara/models/glm-4.7-flash.toml @@ -1,10 +1,14 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.7-flash" structured_output = false -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [cost] input = 0 output = 0 diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml index a1e02119568..118ac8dcb50 100644 --- a/providers/neosantara/models/glm-4.7.toml +++ b/providers/neosantara/models/glm-4.7.toml @@ -1,17 +1,17 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.7" -attachment = true structured_output = false -reasoning_options = [] [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [cost] input = 0.625 output = 1.875 [limit] context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml index 2c8899896e9..55db32f9e29 100644 --- a/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml +++ b/providers/neosantara/models/llama-3.3-nemotron-super-49b-v1.5.toml @@ -1,5 +1,12 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "nvidia/llama-3.3-nemotron-super-49b-v1.5" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.285 diff --git a/providers/neosantara/models/minimax-m3.toml b/providers/neosantara/models/minimax-m3.toml index c6bffed48e2..e82398345b9 100644 --- a/providers/neosantara/models/minimax-m3.toml +++ b/providers/neosantara/models/minimax-m3.toml @@ -1,5 +1,12 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "minimax/MiniMax-M3" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "toggle" [cost] input = 0.3 diff --git a/providers/neosantara/models/muse-glimmer-30b.toml b/providers/neosantara/models/muse-glimmer-30b.toml index 473053dff46..b235b4dbcf9 100644 --- a/providers/neosantara/models/muse-glimmer-30b.toml +++ b/providers/neosantara/models/muse-glimmer-30b.toml @@ -1,5 +1,11 @@ base_model = "meta/muse-glimmer-30b" -reasoning = false + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "xhigh"] [cost] input = 0.35 From 9a923b94d31546203329f2519e9f3a4b9e3fa4da Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 08:13:45 +0000 Subject: [PATCH 25/29] fix(neosantara): verify base_model against local tree and drop redundant structured_output overrides - Confirm catalog base_model exists in local models/ tree before accepting it, falling back safely to id resolution - Drop false structured_output = false overrides across 15 models to inherit structured output support from lab base models - Add unit test coverage for invalid base_model handling and safe fallback --- packages/core/src/sync/providers/neosantara.ts | 10 +++++++--- packages/core/test/neosantara-sync.test.ts | 6 ++++++ providers/neosantara/models/claude-opus-5.toml | 1 - providers/neosantara/models/deepseek-v4-flash.toml | 1 - providers/neosantara/models/deepseek-v4-pro.toml | 1 - providers/neosantara/models/devstral-2.toml | 1 - providers/neosantara/models/glm-4.5-flash.toml | 1 - providers/neosantara/models/glm-4.6v-flash.toml | 1 - providers/neosantara/models/glm-4.7-flash.toml | 1 - providers/neosantara/models/glm-4.7.toml | 1 - providers/neosantara/models/gpt-5.6-luna.toml | 1 - providers/neosantara/models/gpt-5.6-sol.toml | 1 - providers/neosantara/models/gpt-5.6-terra.toml | 1 - providers/neosantara/models/kimi-k2-thinking.toml | 1 - providers/neosantara/models/kimi-k2.5.toml | 1 - providers/neosantara/models/ling-3.0-flash-fin.toml | 1 - providers/neosantara/models/muse-spark-1.1.toml | 1 - 17 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index efbd629451f..5742ba52dd1 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -66,11 +66,15 @@ function mapping(model: NeosantaraSourceModel) { return model.providers[0]; } -// Canonical lab base models are supplied directly by the /v1/catalog endpoint, falling -// back to canonical resolution against the local models/ directory. +// Canonical lab base models are supplied directly by the /v1/catalog endpoint, verified +// against the local models/ directory before falling back to id-based resolution. export function resolveNeosantaraBaseModel(model: NeosantaraSourceModel | string) { if (typeof model === "string") return resolveModelMetadataBaseModel(model); - return model.base_model ?? resolveModelMetadataBaseModel(model.id); + if (model.base_model !== undefined) { + const verified = resolveModelMetadataBaseModel(model.base_model); + if (verified !== undefined) return verified; + } + return resolveModelMetadataBaseModel(model.id); } type ReasoningControls = NonNullable; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 7054db53c72..adfde55838c 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -132,6 +132,12 @@ test("filters to 100k+ context, function calling, image models, and known base m expect( resolveNeosantaraBaseModel({ id: "grok-code-fast", base_model: "xai/grok-4.3" } as never), ).toBe("xai/grok-4.3"); + expect( + resolveNeosantaraBaseModel({ id: "gemini-3.7-flash", base_model: "google/nonexistent-model" } as never), + ).toBe("google/gemini-3.7-flash"); + expect( + resolveNeosantaraBaseModel({ id: "unknown-id", base_model: "nonexistent/fake-model" } as never), + ).toBeUndefined(); expect(resolveNeosantaraBaseModel("definitely-not-a-model")).toBeUndefined(); }); diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 0eb4feacc9c..f12a8d29ae3 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -1,5 +1,4 @@ base_model = "anthropic/claude-opus-5" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 3ae3725a6ff..767d5774ffb 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -2,7 +2,6 @@ # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash-0731" name = "DeepSeek V4 Flash" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index a0a6ec7b6d0..bad2eb48d61 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -2,7 +2,6 @@ # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro-0813" name = "DeepSeek V4 Pro" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/devstral-2.toml b/providers/neosantara/models/devstral-2.toml index 0f8527c3250..19e31206c56 100644 --- a/providers/neosantara/models/devstral-2.toml +++ b/providers/neosantara/models/devstral-2.toml @@ -1,5 +1,4 @@ base_model = "mistral/devstral-2512" -structured_output = false [cost] input = 0.4 diff --git a/providers/neosantara/models/glm-4.5-flash.toml b/providers/neosantara/models/glm-4.5-flash.toml index 478818bf433..d2f6da84cc5 100644 --- a/providers/neosantara/models/glm-4.5-flash.toml +++ b/providers/neosantara/models/glm-4.5-flash.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.5-flash" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/glm-4.6v-flash.toml b/providers/neosantara/models/glm-4.6v-flash.toml index 1cab3f986bc..45ff43eae5b 100644 --- a/providers/neosantara/models/glm-4.6v-flash.toml +++ b/providers/neosantara/models/glm-4.6v-flash.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.6v-flash" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/glm-4.7-flash.toml b/providers/neosantara/models/glm-4.7-flash.toml index 1690bd2ca58..dc643bcc6f6 100644 --- a/providers/neosantara/models/glm-4.7-flash.toml +++ b/providers/neosantara/models/glm-4.7-flash.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.7-flash" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/glm-4.7.toml b/providers/neosantara/models/glm-4.7.toml index 118ac8dcb50..716b59f69a2 100644 --- a/providers/neosantara/models/glm-4.7.toml +++ b/providers/neosantara/models/glm-4.7.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "zhipuai/glm-4.7" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/gpt-5.6-luna.toml b/providers/neosantara/models/gpt-5.6-luna.toml index 188399c8550..a7094fb9a76 100644 --- a/providers/neosantara/models/gpt-5.6-luna.toml +++ b/providers/neosantara/models/gpt-5.6-luna.toml @@ -1,7 +1,6 @@ base_model = "openai/gpt-5.6-luna" base_model_omit = ["limit.input"] attachment = false -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/gpt-5.6-sol.toml b/providers/neosantara/models/gpt-5.6-sol.toml index e4e71af98a5..d7235e662e9 100644 --- a/providers/neosantara/models/gpt-5.6-sol.toml +++ b/providers/neosantara/models/gpt-5.6-sol.toml @@ -1,7 +1,6 @@ base_model = "openai/gpt-5.6-sol" base_model_omit = ["limit.input"] attachment = false -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index a2f93470a5c..84820250e5c 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -1,6 +1,5 @@ base_model = "openai/gpt-5.6-terra" base_model_omit = ["limit.input"] -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index 62d92c7bac9..88d416695f5 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,5 +1,4 @@ base_model = "moonshotai/kimi-k2-thinking" -structured_output = false reasoning_options = [] [interleaved] diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 0b4afeeb15c..d451cf021ce 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k2.5" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/ling-3.0-flash-fin.toml b/providers/neosantara/models/ling-3.0-flash-fin.toml index 252b2f70bc9..756cf791b35 100644 --- a/providers/neosantara/models/ling-3.0-flash-fin.toml +++ b/providers/neosantara/models/ling-3.0-flash-fin.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "inclusionai/ling-3.0-flash-fin" -structured_output = false [interleaved] field = "reasoning_content" diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index c81c2320be0..a62e2e536c1 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -1,5 +1,4 @@ base_model = "meta/muse-spark-1.1" -structured_output = false [interleaved] field = "reasoning_content" From 260be3381d972c970108ffe895a6aae5c2700ec0 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 10:10:08 +0000 Subject: [PATCH 26/29] fix(neosantara): deploy text-only modalities on non-vision models and align graded reasoning - Add deploymentModalities to strip image/pdf/video inputs when vision is false, preventing multimodal inheritance alongside attachment = false - Treat vision and tools as optional, removing redundant [modalities] overrides on multimodal lab models - Align reasoning controls on claude-4.5-opus, claude-opus-4-6, and claude-sonnet-4-6 to effort with none, eliminating invented toggle hybrids --- .../core/src/sync/providers/neosantara.ts | 29 +++++++-- packages/core/test/neosantara-sync.test.ts | 64 +++++++++++++++++++ .../neosantara/models/claude-4.5-opus.toml | 10 +-- .../neosantara/models/claude-4.5-sonnet.toml | 3 - .../neosantara/models/claude-opus-4-6.toml | 7 +- .../neosantara/models/claude-opus-4-7.toml | 3 - .../neosantara/models/claude-opus-4-8.toml | 3 - .../neosantara/models/claude-opus-5.toml | 3 - .../neosantara/models/claude-sonnet-4-6.toml | 7 +- .../neosantara/models/claude-sonnet-5.toml | 3 - .../models/gemini-3.1-flash-lite.toml | 3 - .../neosantara/models/gemini-3.5-flash.toml | 3 - .../neosantara/models/gemini-3.6-flash.toml | 3 - .../neosantara/models/gemini-3.7-flash.toml | 3 - .../neosantara/models/gpt-5.6-terra.toml | 3 - providers/neosantara/models/kimi-k2.5.toml | 3 - providers/neosantara/models/minimax-m3.toml | 3 - .../neosantara/models/muse-spark-1.1.toml | 3 - 18 files changed, 90 insertions(+), 66 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 5742ba52dd1..fc0dd3c415d 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -156,6 +156,26 @@ function modalities(values: string[]): string[] { return [...new Set(mapped.length > 0 ? mapped : ["text"])]; } +// Modalities as served by a specific deployment: a mapping without vision must +// not carry image/pdf/video input, regardless of what the model-level architecture +// claims — attachment=false with image/pdf/video input is contradictory. +export function deploymentModalities(model: NeosantaraSourceModel, vision: boolean | undefined) { + const baseInput = modalities(model.architecture.input_modalities); + if (vision !== false) { + return { + input: baseInput, + output: modalities(model.architecture.output_modalities), + }; + } + const filtered = baseInput.filter( + (value) => value !== "image" && value !== "pdf" && value !== "video", + ); + return { + input: filtered.length > 0 ? filtered : ["text"], + output: modalities(model.architecture.output_modalities), + }; +} + // Image generators, or 100k+ context function-calling text models. export function meetsNeosantaraPublicFilter(model: NeosantaraSourceModel) { return ( @@ -216,13 +236,10 @@ export function buildNeosantaraModel( reasoning, reasoning_options: reasoning ? neosantaraReasoningControls(model, existing) : undefined, interleaved: reasoning ? { field: "reasoning_content" as const } : undefined, - attachment: map.vision === true, - tool_call: map.tools === true, + attachment: map.vision, + tool_call: map.tools, structured_output: model.structured_outputs === false ? false : undefined, - modalities: { - input: modalities(model.architecture.input_modalities), - output: modalities(model.architecture.output_modalities), - }, + modalities: map.vision === false ? deploymentModalities(model, false) : undefined, status: model.deprecated ? "deprecated" : existing?.status, limit, cost: { diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index adfde55838c..bdf26705189 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "bun:test"; import { buildNeosantaraModel, + deploymentModalities, meetsNeosantaraPublicFilter, neosantara, neosantaraReasoningControls, @@ -310,3 +311,66 @@ test("filters deprecated models and reports unmapped ids as skipped", () => { test("an empty catalog is fatal rather than wiping every model", () => { expect(() => neosantara.parseModels({ data: [] })).toThrow(); }); + +test("deploymentModalities strips non-text inputs when vision is false and preserves otherwise", () => { + const multimodal = { + architecture: { + input_modalities: ["text", "image", "video", "pdf"], + output_modalities: ["text"], + }, + } as never; + + expect(deploymentModalities(multimodal, false)).toEqual({ + input: ["text"], + output: ["text"], + }); + + expect(deploymentModalities(multimodal, true)).toEqual({ + input: ["text", "image", "video", "pdf"], + output: ["text"], + }); + + expect(deploymentModalities(multimodal, undefined)).toEqual({ + input: ["text", "image", "video", "pdf"], + output: ["text"], + }); +}); + +test("enforces text-only input and attachment=false when vision is false on multimodal base model", () => { + const model = { + id: "kimi-k3", + context_length: 1_048_576, + architecture: { input_modalities: ["text"], output_modalities: ["text"] }, + pricing: { prompt: "0.000003000000", completion: "0.000015000000" }, + providers: [{ providerId: "neosantara", vision: false, tools: true, reasoning: false }], + } as never; + + const built = buildNeosantaraModel(model, undefined); + // moonshotai/kimi-k3 base model has attachment=true and input=["text", "image", "video"]. + // Because vision is false on this host, attachment must be false AND input must be overridden to ["text"]. + expect(built).toMatchObject({ + base_model: "moonshotai/kimi-k3", + attachment: false, + modalities: { input: ["text"] }, + }); + // tool_call is true in both host and base model; factored out + expect("tool_call" in built).toBe(false); +}); + +test("omits attachment and modalities when vision is true matching base model", () => { + const model = { + id: "claude-4.5-opus", + base_model: "anthropic/claude-opus-4-5", + context_length: 200_000, + architecture: { input_modalities: ["text", "image"], output_modalities: ["text"] }, + pricing: { prompt: "0.000005000000", completion: "0.000025000000" }, + providers: [{ providerId: "neosantara", vision: true, tools: true, reasoning: false }], + } as never; + + const built = buildNeosantaraModel(model, undefined); + // anthropic/claude-opus-4-5 base model has attachment=true and input=["text", "image", "pdf"]. + // Because vision is true, attachment and modalities overrides are omitted and inherited from the base model. + expect("attachment" in built).toBe(false); + expect("modalities" in built).toBe(false); +}); + diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index 3dc5d649fe3..bf492e37b46 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,22 +1,14 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-5" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none", "low", "medium", "high"] [cost] input = 5 output = 25 cache_read = 0.5 cache_write = 6.25 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml index ff2be1b7756..a4bde261f8a 100644 --- a/providers/neosantara/models/claude-4.5-sonnet.toml +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -13,6 +13,3 @@ input = 3 output = 15 cache_read = 0.3 cache_write = 3.75 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index aabe1d6d22c..cc57c65d369 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,17 +1,12 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-6" attachment = false [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index edf73248854..dfb9e63ac2b 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -15,6 +15,3 @@ cache_write = 6.25 [limit] context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index 752a1fee6bb..fca64bdab1b 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -15,6 +15,3 @@ cache_write = 6.25 [limit] context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index f12a8d29ae3..4f20e8b0ee0 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -10,6 +10,3 @@ values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 output = 25 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 03860c5d20d..40159a0b047 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,17 +1,12 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-4-6" attachment = false [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 3 diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index 5fb4b45d8c0..b9dc356327e 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -20,6 +20,3 @@ cache_write = 3.75 [limit] context = 200_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.1-flash-lite.toml b/providers/neosantara/models/gemini-3.1-flash-lite.toml index 0e87adb20c3..acbd32a1764 100644 --- a/providers/neosantara/models/gemini-3.1-flash-lite.toml +++ b/providers/neosantara/models/gemini-3.1-flash-lite.toml @@ -14,6 +14,3 @@ cache_read = 0.025 [limit] context = 1_000_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.5-flash.toml b/providers/neosantara/models/gemini-3.5-flash.toml index 4b6ba44c0a3..b769569f475 100644 --- a/providers/neosantara/models/gemini-3.5-flash.toml +++ b/providers/neosantara/models/gemini-3.5-flash.toml @@ -15,6 +15,3 @@ cache_write = 1.5 [limit] context = 1_000_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 4065d09c643..9702a1ac44a 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -15,6 +15,3 @@ cache_write = 0.75 [limit] context = 1_000_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 46aa16386a5..51f9b45a9d2 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -15,6 +15,3 @@ cache_write = 0.75 [limit] context = 1_000_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index 84820250e5c..58e488dfcb9 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -14,6 +14,3 @@ output = 12 [limit] context = 272_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index d451cf021ce..0d2d562f97a 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -14,6 +14,3 @@ output = 3 [limit] context = 256_000 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/minimax-m3.toml b/providers/neosantara/models/minimax-m3.toml index e82398345b9..387d2222f87 100644 --- a/providers/neosantara/models/minimax-m3.toml +++ b/providers/neosantara/models/minimax-m3.toml @@ -15,6 +15,3 @@ cache_read = 0.06 [limit] context = 524_288 - -[modalities] -input = ["text", "image"] diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index a62e2e536c1..5e893bdb8cb 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -11,6 +11,3 @@ values = ["minimal", "low", "medium", "high", "xhigh"] input = 1.25 output = 4.25 cache_read = 0.15 - -[modalities] -input = ["text", "image"] From d11f6ef79f75d646b740528acde55694c6ec7f93 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 10:54:48 +0000 Subject: [PATCH 27/29] fix(neosantara): inherit lab modalities on vision models and align Claude reasoning controls - Inherit lab multimodal inputs and drop attachment = false on vision-capable models (claude-opus-4-6, claude-sonnet-4-6, claude-fable-5, glm-5.3-flash, gpt-5.4, gpt-5.5, gpt-5.4-mini, kimi-k2.6, kimi-k3) - Align Claude reasoning controls on claude-opus-4-6, claude-sonnet-4-6, and claude-4.5-opus to toggle + graded effort without none, accompanied by wire comment - Add dedicated headers documenting always-on thinking on minimax-m2.7 and citing Meta model card/peer baselines for muse-glimmer-30b - Regenerate model TOMLs ensuring translator and catalog remain 100% idempotent --- .../core/src/sync/providers/neosantara.ts | 27 ++++++++++++++++--- packages/core/test/neosantara-sync.test.ts | 7 +++++ .../neosantara/models/claude-4.5-opus.toml | 7 ++++- .../neosantara/models/claude-fable-5.toml | 4 --- .../neosantara/models/claude-opus-4-6.toml | 11 ++++---- .../neosantara/models/claude-sonnet-4-6.toml | 11 ++++---- .../neosantara/models/glm-5.3-flash.toml | 4 --- providers/neosantara/models/gpt-5.4-mini.toml | 4 --- providers/neosantara/models/gpt-5.4.toml | 4 --- providers/neosantara/models/gpt-5.5.toml | 4 --- providers/neosantara/models/kimi-k2.6.toml | 4 --- providers/neosantara/models/kimi-k3.toml | 4 --- providers/neosantara/models/minimax-m2.7.toml | 2 ++ .../neosantara/models/muse-glimmer-30b.toml | 4 +++ 14 files changed, 54 insertions(+), 43 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index fc0dd3c415d..731335003de 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -121,9 +121,28 @@ export function neosantaraReasoningControls( return [{ type: "effort", values: efforts as never }]; } -// A toggle needs its wire comment; effort/always-on models carry none. -export function neosantaraReasoningHeader(controls: ReasoningControls | undefined) { - return controls?.some((option) => option.type === "toggle") ? TOGGLE_HEADER : undefined; +// Toggle controls carry a leading wire comment naming the off value (AGENTS.md requirement). +// Dedicated headers document always-on relays (minimax-m2.7) and external effort baselines (muse-glimmer-30b). +export function neosantaraReasoningHeader( + controls: ReasoningControls | undefined, + modelId?: string, +) { + if (controls?.some((option) => option.type === "toggle")) { + return TOGGLE_HEADER; + } + if (modelId === "minimax-m2.7") { + return `# Always-on thinking: upstream Dahl forwards no reasoning_effort parameter. +# Matches lab providers/minimax/models/MiniMax-M2.7.toml and peers OpenRouter/FastRouter/Cortecs. +`; + } + if (modelId === "muse-glimmer-30b") { + return `# Sources: +# https://huggingface.co/meta-models/Muse-Glimmer-30B +# Effort: reasoning_effort = low|medium|high|xhigh +# Matches peers OpenRouter and Vercel AI Gateway. +`; + } + return undefined; } // Image models output an image modality, are priced per image, and skip the token filters. @@ -297,7 +316,7 @@ export const neosantara = { return { id: model.id, model: translated, - header: neosantaraReasoningHeader(translated.reasoning_options), + header: neosantaraReasoningHeader(translated.reasoning_options, model.id), }; }, } satisfies SyncProvider; diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index bdf26705189..a675a807009 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -265,6 +265,13 @@ test("toggle models carry a wire-comment header; effort models carry none", () = // gemini-3.7-flash advertises graded effort -> no toggle header. const effort = neosantara.translateModel(models[0]!, ctx); expect(effort?.header ?? "").not.toContain("Toggle"); + + // minimax-m2.7 documents always-on reasoning. + expect(neosantaraReasoningHeader([], "minimax-m2.7")).toContain("Always-on thinking"); + // muse-glimmer-30b documents external baseline sources. + expect(neosantaraReasoningHeader([{ type: "effort", values: ["low"] as never }], "muse-glimmer-30b")).toContain( + "https://huggingface.co/meta-models/Muse-Glimmer-30B", + ); }); test("syncs image-generation models on per-image pricing without token cost", () => { diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index bf492e37b46..2bb742f8e2b 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,11 +1,16 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-5" [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high"] +values = ["low", "medium", "high"] [cost] input = 5 diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 64cc050068d..70a641e08c0 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -1,5 +1,4 @@ base_model = "anthropic/claude-fable-5" -attachment = false [interleaved] field = "reasoning_content" @@ -16,6 +15,3 @@ cache_write = 12.5 [limit] context = 200_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index cc57c65d369..aedd7c45788 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,12 +1,16 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-6" -attachment = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["low", "medium", "high", "max"] [cost] input = 5 @@ -16,6 +20,3 @@ cache_write = 6.25 [limit] context = 200_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index 40159a0b047..cc772eb31bb 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,12 +1,16 @@ +# Toggle: reasoning_effort = "none" turns thinking off; any other accepted +# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-4-6" -attachment = false [interleaved] field = "reasoning_content" +[[reasoning_options]] +type = "toggle" + [[reasoning_options]] type = "effort" -values = ["none", "low", "medium", "high", "max"] +values = ["low", "medium", "high", "max"] [cost] input = 3 @@ -16,6 +20,3 @@ cache_write = 3.75 [limit] context = 200_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml index c3de88decf7..fc13e4d4e4d 100644 --- a/providers/neosantara/models/glm-5.3-flash.toml +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -1,5 +1,4 @@ base_model = "zhipuai/glm-5.3-flash" -attachment = false [interleaved] field = "reasoning_content" @@ -12,6 +11,3 @@ values = ["low", "high", "max"] input = 0.15 output = 0.5 cache_read = 0.03 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/gpt-5.4-mini.toml b/providers/neosantara/models/gpt-5.4-mini.toml index c8cfd0370e5..43dfdad1d82 100644 --- a/providers/neosantara/models/gpt-5.4-mini.toml +++ b/providers/neosantara/models/gpt-5.4-mini.toml @@ -1,6 +1,5 @@ base_model = "openai/gpt-5.4-mini" base_model_omit = ["limit.input"] -attachment = false [interleaved] field = "reasoning_content" @@ -17,6 +16,3 @@ cache_write = 0.75 [limit] context = 128_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 67d20995843..62068a58b0b 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -1,6 +1,5 @@ base_model = "openai/gpt-5.4" base_model_omit = ["limit.input"] -attachment = false [interleaved] field = "reasoning_content" @@ -16,6 +15,3 @@ cache_read = 0.25 [limit] context = 270_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index 3b86415307d..96edcd0c463 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -1,6 +1,5 @@ base_model = "openai/gpt-5.5" base_model_omit = ["limit.input"] -attachment = false [interleaved] field = "reasoning_content" @@ -16,6 +15,3 @@ cache_read = 0.5 [limit] context = 270_000 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 6028ae5d113..200d8147f18 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k2.6" -attachment = false [interleaved] field = "reasoning_content" @@ -12,6 +11,3 @@ type = "toggle" [cost] input = 0.285 output = 1.2 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index c3d86a4cb06..6c5471b1a6a 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,7 +1,6 @@ # Toggle: reasoning_effort = "none" turns thinking off; any other accepted # value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k3" -attachment = false [interleaved] field = "reasoning_content" @@ -17,6 +16,3 @@ values = ["low", "high", "max"] input = 3 output = 15 cache_read = 0.3 - -[modalities] -input = ["text"] diff --git a/providers/neosantara/models/minimax-m2.7.toml b/providers/neosantara/models/minimax-m2.7.toml index 53be34d1e67..c92cde098a0 100644 --- a/providers/neosantara/models/minimax-m2.7.toml +++ b/providers/neosantara/models/minimax-m2.7.toml @@ -1,3 +1,5 @@ +# Always-on thinking: upstream Dahl forwards no reasoning_effort parameter. +# Matches lab providers/minimax/models/MiniMax-M2.7.toml and peers OpenRouter/FastRouter/Cortecs. base_model = "minimax/MiniMax-M2.7" reasoning_options = [] diff --git a/providers/neosantara/models/muse-glimmer-30b.toml b/providers/neosantara/models/muse-glimmer-30b.toml index b235b4dbcf9..63fdbc24b80 100644 --- a/providers/neosantara/models/muse-glimmer-30b.toml +++ b/providers/neosantara/models/muse-glimmer-30b.toml @@ -1,3 +1,7 @@ +# Sources: +# https://huggingface.co/meta-models/Muse-Glimmer-30B +# Effort: reasoning_effort = low|medium|high|xhigh +# Matches peers OpenRouter and Vercel AI Gateway. base_model = "meta/muse-glimmer-30b" [interleaved] From a91c4c9dda1f1d8dcee043b7e62237ed8c23c5b1 Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 11:50:25 +0000 Subject: [PATCH 28/29] fix(neosantara): author effort with none without toggle and match narrower deployment modalities --- .../core/src/sync/providers/neosantara.ts | 21 +++---------- packages/core/test/neosantara-sync.test.ts | 31 +++++++++++++------ .../neosantara/models/claude-4.5-opus.toml | 10 +++--- .../neosantara/models/claude-4.5-sonnet.toml | 3 ++ .../neosantara/models/claude-fable-5.toml | 3 ++ .../neosantara/models/claude-opus-4-6.toml | 10 +++--- .../neosantara/models/claude-opus-4-7.toml | 3 ++ .../neosantara/models/claude-opus-4-8.toml | 3 ++ .../neosantara/models/claude-opus-5.toml | 3 ++ .../neosantara/models/claude-sonnet-4-6.toml | 10 +++--- .../neosantara/models/claude-sonnet-5.toml | 10 +++--- .../models/deepseek-v4-flash-0731.toml | 7 +---- .../neosantara/models/deepseek-v4-flash.toml | 7 +---- .../models/deepseek-v4-pro-0813.toml | 7 +---- .../neosantara/models/deepseek-v4-pro.toml | 7 +---- .../models/gemini-3.1-flash-lite.toml | 3 ++ .../neosantara/models/gemini-3.5-flash.toml | 3 ++ .../neosantara/models/gemini-3.6-flash.toml | 3 ++ .../neosantara/models/gemini-3.7-flash.toml | 3 ++ .../neosantara/models/glm-5.3-flash.toml | 3 ++ providers/neosantara/models/gpt-5.4.toml | 3 ++ providers/neosantara/models/gpt-5.5.toml | 3 ++ .../neosantara/models/gpt-5.6-terra.toml | 3 ++ providers/neosantara/models/kimi-k2.5.toml | 3 ++ providers/neosantara/models/kimi-k2.6.toml | 3 ++ providers/neosantara/models/kimi-k3.toml | 10 +++--- providers/neosantara/models/minimax-m3.toml | 3 ++ .../neosantara/models/muse-spark-1.1.toml | 3 ++ 28 files changed, 102 insertions(+), 79 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index 731335003de..d879297bf39 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -92,8 +92,8 @@ function hasReasoningEfforts(model: NeosantaraSourceModel) { // The catalog reports each model's real reasoning surface (resolved host-side from the model's // models.dev lab entry): [] = always-on (no caller control), ["none"] = on/off toggle, otherwise -// the graded effort levels. When a model's lab entry exposes toggle + graded effort (e.g. Sonnet 5), -// the host reports `has_toggle: true`, authoring `toggle` alongside the graded effort ladder. +// the graded effort levels. When off is expressed via reasoning_effort = "none" alongside graded +// levels, AGENTS.md strictly requires authoring only `effort` with `none` in values and NO toggle. export function neosantaraReasoningControls( model: NeosantaraSourceModel, _existing?: ExistingModel, @@ -105,20 +105,9 @@ export function neosantaraReasoningControls( if (efforts.length === 0) return []; if (efforts.length === 1 && efforts[0] === "none") return [{ type: "toggle" }]; - const hasToggle = map.has_toggle === true; const graded = efforts.filter((effort) => effort !== "none"); - - if (hasToggle) { - if (graded.length > 0) { - return [ - { type: "toggle" }, - { type: "effort", values: graded as never }, - ]; - } - return [{ type: "toggle" }]; - } - - return [{ type: "effort", values: efforts as never }]; + const values = efforts.includes("none") ? ["none", ...graded] : graded; + return [{ type: "effort", values: values as never }]; } // Toggle controls carry a leading wire comment naming the off value (AGENTS.md requirement). @@ -258,7 +247,7 @@ export function buildNeosantaraModel( attachment: map.vision, tool_call: map.tools, structured_output: model.structured_outputs === false ? false : undefined, - modalities: map.vision === false ? deploymentModalities(model, false) : undefined, + modalities: deploymentModalities(model, map.vision), status: model.deprecated ? "deprecated" : existing?.status, limit, cost: { diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index a675a807009..9fddb2dd2c3 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -202,24 +202,21 @@ test("copies the catalog's per-model reasoning surface verbatim (llmgateway conv expect(ctl(["none", "high", "max"])).toEqual([{ type: "effort", values: ["none", "high", "max"] }]); expect(ctl(["low", "high", "max", "bogus"])).toEqual([{ type: "effort", values: ["low", "high", "max"] }]); - // When a model has has_toggle: true alongside graded levels, emit toggle + effort. + // Graded levels with "none" author only effort with "none" in values, without toggle. expect( neosantaraReasoningControls({ - providers: [{ reasoning: true, reasoning_efforts: ["low", "medium", "high", "xhigh", "max"], has_toggle: true }], + providers: [{ reasoning: true, reasoning_efforts: ["low", "medium", "high", "xhigh", "max"] }], } as never), ).toEqual([ - { type: "toggle" }, { type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }, ]); - // When a model has has_toggle: true and efforts contains "none", emit toggle + effort without "none". expect( neosantaraReasoningControls({ - providers: [{ reasoning: true, reasoning_efforts: ["none", "low", "high", "max"], has_toggle: true }], + providers: [{ reasoning: true, reasoning_efforts: ["none", "low", "high", "max"] }], } as never), ).toEqual([ - { type: "toggle" }, - { type: "effort", values: ["low", "high", "max"] }, + { type: "effort", values: ["none", "low", "high", "max"] }, ]); // Toggle is driven only by the catalog (has_toggle / ["none"]), not re-inferred from prior TOMLs. @@ -364,7 +361,7 @@ test("enforces text-only input and attachment=false when vision is false on mult expect("tool_call" in built).toBe(false); }); -test("omits attachment and modalities when vision is true matching base model", () => { +test("emits narrower modalities when host deployment does not support pdf/video", () => { const model = { id: "claude-4.5-opus", base_model: "anthropic/claude-opus-4-5", @@ -376,7 +373,23 @@ test("omits attachment and modalities when vision is true matching base model", const built = buildNeosantaraModel(model, undefined); // anthropic/claude-opus-4-5 base model has attachment=true and input=["text", "image", "pdf"]. - // Because vision is true, attachment and modalities overrides are omitted and inherited from the base model. + // Because vision is true, attachment is true matching base and factored out. + // Because host deployment only serves text+image (not pdf), narrower input modalities are authored. + expect("attachment" in built).toBe(false); + expect(built.modalities).toEqual({ input: ["text", "image"] }); +}); + +test("omits modalities when host input modalities match base model", () => { + const model = { + id: "claude-4.5-opus", + base_model: "anthropic/claude-opus-4-5", + context_length: 200_000, + architecture: { input_modalities: ["text", "image", "pdf"], output_modalities: ["text"] }, + pricing: { prompt: "0.000005000000", completion: "0.000025000000" }, + providers: [{ providerId: "neosantara", vision: true, tools: true, reasoning: false }], + } as never; + + const built = buildNeosantaraModel(model, undefined); expect("attachment" in built).toBe(false); expect("modalities" in built).toBe(false); }); diff --git a/providers/neosantara/models/claude-4.5-opus.toml b/providers/neosantara/models/claude-4.5-opus.toml index 2bb742f8e2b..a4d4256c940 100644 --- a/providers/neosantara/models/claude-4.5-opus.toml +++ b/providers/neosantara/models/claude-4.5-opus.toml @@ -1,19 +1,17 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-5" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high"] +values = ["none", "low", "medium", "high"] [cost] input = 5 output = 25 cache_read = 0.5 cache_write = 6.25 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-4.5-sonnet.toml b/providers/neosantara/models/claude-4.5-sonnet.toml index a4bde261f8a..ff2be1b7756 100644 --- a/providers/neosantara/models/claude-4.5-sonnet.toml +++ b/providers/neosantara/models/claude-4.5-sonnet.toml @@ -13,3 +13,6 @@ input = 3 output = 15 cache_read = 0.3 cache_write = 3.75 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-fable-5.toml b/providers/neosantara/models/claude-fable-5.toml index 70a641e08c0..975c0ff8372 100644 --- a/providers/neosantara/models/claude-fable-5.toml +++ b/providers/neosantara/models/claude-fable-5.toml @@ -15,3 +15,6 @@ cache_write = 12.5 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-6.toml b/providers/neosantara/models/claude-opus-4-6.toml index aedd7c45788..855d25016e7 100644 --- a/providers/neosantara/models/claude-opus-4-6.toml +++ b/providers/neosantara/models/claude-opus-4-6.toml @@ -1,16 +1,11 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-opus-4-6" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 5 @@ -20,3 +15,6 @@ cache_write = 6.25 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-7.toml b/providers/neosantara/models/claude-opus-4-7.toml index dfb9e63ac2b..edf73248854 100644 --- a/providers/neosantara/models/claude-opus-4-7.toml +++ b/providers/neosantara/models/claude-opus-4-7.toml @@ -15,3 +15,6 @@ cache_write = 6.25 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-4-8.toml b/providers/neosantara/models/claude-opus-4-8.toml index fca64bdab1b..752a1fee6bb 100644 --- a/providers/neosantara/models/claude-opus-4-8.toml +++ b/providers/neosantara/models/claude-opus-4-8.toml @@ -15,3 +15,6 @@ cache_write = 6.25 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-opus-5.toml b/providers/neosantara/models/claude-opus-5.toml index 4f20e8b0ee0..f12a8d29ae3 100644 --- a/providers/neosantara/models/claude-opus-5.toml +++ b/providers/neosantara/models/claude-opus-5.toml @@ -10,3 +10,6 @@ values = ["low", "medium", "high", "xhigh", "max"] [cost] input = 5 output = 25 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-sonnet-4-6.toml b/providers/neosantara/models/claude-sonnet-4-6.toml index cc772eb31bb..1744aacaf58 100644 --- a/providers/neosantara/models/claude-sonnet-4-6.toml +++ b/providers/neosantara/models/claude-sonnet-4-6.toml @@ -1,16 +1,11 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-4-6" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "max"] +values = ["none", "low", "medium", "high", "max"] [cost] input = 3 @@ -20,3 +15,6 @@ cache_write = 3.75 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/claude-sonnet-5.toml b/providers/neosantara/models/claude-sonnet-5.toml index b9dc356327e..b3e3e1cc876 100644 --- a/providers/neosantara/models/claude-sonnet-5.toml +++ b/providers/neosantara/models/claude-sonnet-5.toml @@ -1,16 +1,11 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "anthropic/claude-sonnet-5" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "medium", "high", "xhigh", "max"] +values = ["none", "low", "medium", "high", "xhigh", "max"] [cost] input = 3 @@ -20,3 +15,6 @@ cache_write = 3.75 [limit] context = 200_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/deepseek-v4-flash-0731.toml b/providers/neosantara/models/deepseek-v4-flash-0731.toml index 6b1aa0c13f1..41f59d2d4a0 100644 --- a/providers/neosantara/models/deepseek-v4-flash-0731.toml +++ b/providers/neosantara/models/deepseek-v4-flash-0731.toml @@ -1,16 +1,11 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash-0731" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-flash.toml b/providers/neosantara/models/deepseek-v4-flash.toml index 767d5774ffb..52a52f369f2 100644 --- a/providers/neosantara/models/deepseek-v4-flash.toml +++ b/providers/neosantara/models/deepseek-v4-flash.toml @@ -1,17 +1,12 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-flash-0731" name = "DeepSeek V4 Flash" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 0.14 diff --git a/providers/neosantara/models/deepseek-v4-pro-0813.toml b/providers/neosantara/models/deepseek-v4-pro-0813.toml index dce1a846cbd..e88728c0359 100644 --- a/providers/neosantara/models/deepseek-v4-pro-0813.toml +++ b/providers/neosantara/models/deepseek-v4-pro-0813.toml @@ -1,16 +1,11 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro-0813" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["high", "max"] +values = ["none", "high", "max"] [cost] input = 1.32 diff --git a/providers/neosantara/models/deepseek-v4-pro.toml b/providers/neosantara/models/deepseek-v4-pro.toml index bad2eb48d61..9ed5e2a9018 100644 --- a/providers/neosantara/models/deepseek-v4-pro.toml +++ b/providers/neosantara/models/deepseek-v4-pro.toml @@ -1,17 +1,12 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "deepseek/deepseek-v4-pro-0813" name = "DeepSeek V4 Pro" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["high", "max"] +values = ["none", "high", "max"] [cost] input = 2.4 diff --git a/providers/neosantara/models/gemini-3.1-flash-lite.toml b/providers/neosantara/models/gemini-3.1-flash-lite.toml index acbd32a1764..0e87adb20c3 100644 --- a/providers/neosantara/models/gemini-3.1-flash-lite.toml +++ b/providers/neosantara/models/gemini-3.1-flash-lite.toml @@ -14,3 +14,6 @@ cache_read = 0.025 [limit] context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.5-flash.toml b/providers/neosantara/models/gemini-3.5-flash.toml index b769569f475..4b6ba44c0a3 100644 --- a/providers/neosantara/models/gemini-3.5-flash.toml +++ b/providers/neosantara/models/gemini-3.5-flash.toml @@ -15,3 +15,6 @@ cache_write = 1.5 [limit] context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.6-flash.toml b/providers/neosantara/models/gemini-3.6-flash.toml index 9702a1ac44a..4065d09c643 100644 --- a/providers/neosantara/models/gemini-3.6-flash.toml +++ b/providers/neosantara/models/gemini-3.6-flash.toml @@ -15,3 +15,6 @@ cache_write = 0.75 [limit] context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gemini-3.7-flash.toml b/providers/neosantara/models/gemini-3.7-flash.toml index 51f9b45a9d2..46aa16386a5 100644 --- a/providers/neosantara/models/gemini-3.7-flash.toml +++ b/providers/neosantara/models/gemini-3.7-flash.toml @@ -15,3 +15,6 @@ cache_write = 0.75 [limit] context = 1_000_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/glm-5.3-flash.toml b/providers/neosantara/models/glm-5.3-flash.toml index fc13e4d4e4d..a09d695afea 100644 --- a/providers/neosantara/models/glm-5.3-flash.toml +++ b/providers/neosantara/models/glm-5.3-flash.toml @@ -11,3 +11,6 @@ values = ["low", "high", "max"] input = 0.15 output = 0.5 cache_read = 0.03 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gpt-5.4.toml b/providers/neosantara/models/gpt-5.4.toml index 62068a58b0b..148f0ef35b4 100644 --- a/providers/neosantara/models/gpt-5.4.toml +++ b/providers/neosantara/models/gpt-5.4.toml @@ -15,3 +15,6 @@ cache_read = 0.25 [limit] context = 270_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gpt-5.5.toml b/providers/neosantara/models/gpt-5.5.toml index 96edcd0c463..8c2ec38ca96 100644 --- a/providers/neosantara/models/gpt-5.5.toml +++ b/providers/neosantara/models/gpt-5.5.toml @@ -15,3 +15,6 @@ cache_read = 0.5 [limit] context = 270_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/gpt-5.6-terra.toml b/providers/neosantara/models/gpt-5.6-terra.toml index 58e488dfcb9..84820250e5c 100644 --- a/providers/neosantara/models/gpt-5.6-terra.toml +++ b/providers/neosantara/models/gpt-5.6-terra.toml @@ -14,3 +14,6 @@ output = 12 [limit] context = 272_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/kimi-k2.5.toml b/providers/neosantara/models/kimi-k2.5.toml index 0d2d562f97a..d451cf021ce 100644 --- a/providers/neosantara/models/kimi-k2.5.toml +++ b/providers/neosantara/models/kimi-k2.5.toml @@ -14,3 +14,6 @@ output = 3 [limit] context = 256_000 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/kimi-k2.6.toml b/providers/neosantara/models/kimi-k2.6.toml index 200d8147f18..f0758670743 100644 --- a/providers/neosantara/models/kimi-k2.6.toml +++ b/providers/neosantara/models/kimi-k2.6.toml @@ -11,3 +11,6 @@ type = "toggle" [cost] input = 0.285 output = 1.2 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/kimi-k3.toml b/providers/neosantara/models/kimi-k3.toml index 6c5471b1a6a..d6c9b2067ad 100644 --- a/providers/neosantara/models/kimi-k3.toml +++ b/providers/neosantara/models/kimi-k3.toml @@ -1,18 +1,16 @@ -# Toggle: reasoning_effort = "none" turns thinking off; any other accepted -# value turns it on. https://docs.neosantara.xyz/en/capability/reasoning base_model = "moonshotai/kimi-k3" [interleaved] field = "reasoning_content" -[[reasoning_options]] -type = "toggle" - [[reasoning_options]] type = "effort" -values = ["low", "high", "max"] +values = ["none", "low", "high", "max"] [cost] input = 3 output = 15 cache_read = 0.3 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/minimax-m3.toml b/providers/neosantara/models/minimax-m3.toml index 387d2222f87..e82398345b9 100644 --- a/providers/neosantara/models/minimax-m3.toml +++ b/providers/neosantara/models/minimax-m3.toml @@ -15,3 +15,6 @@ cache_read = 0.06 [limit] context = 524_288 + +[modalities] +input = ["text", "image"] diff --git a/providers/neosantara/models/muse-spark-1.1.toml b/providers/neosantara/models/muse-spark-1.1.toml index 5e893bdb8cb..a62e2e536c1 100644 --- a/providers/neosantara/models/muse-spark-1.1.toml +++ b/providers/neosantara/models/muse-spark-1.1.toml @@ -11,3 +11,6 @@ values = ["minimal", "low", "medium", "high", "xhigh"] input = 1.25 output = 4.25 cache_read = 0.15 + +[modalities] +input = ["text", "image"] From aae038030d4e741e35a2b1e7017c6cd3319207ca Mon Sep 17 00:00:00 2001 From: ErRickow Date: Fri, 4 Sep 2026 11:55:15 +0000 Subject: [PATCH 29/29] docs(neosantara): add always-on reasoning header for kimi-k2-thinking and update provider comments --- packages/core/src/sync/providers/neosantara.ts | 5 +++++ packages/core/test/neosantara-sync.test.ts | 2 ++ providers/neosantara/models/kimi-k2-thinking.toml | 2 ++ providers/neosantara/provider.toml | 7 ++++--- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/core/src/sync/providers/neosantara.ts b/packages/core/src/sync/providers/neosantara.ts index d879297bf39..8c1fc5d7a59 100644 --- a/packages/core/src/sync/providers/neosantara.ts +++ b/packages/core/src/sync/providers/neosantara.ts @@ -122,6 +122,11 @@ export function neosantaraReasoningHeader( if (modelId === "minimax-m2.7") { return `# Always-on thinking: upstream Dahl forwards no reasoning_effort parameter. # Matches lab providers/minimax/models/MiniMax-M2.7.toml and peers OpenRouter/FastRouter/Cortecs. +`; + } + if (modelId === "kimi-k2-thinking") { + return `# Dedicated thinking variant: always-on reasoning with no caller control. +# Matches lab providers/moonshotai/models/kimi-k2-thinking.toml and peers OpenRouter/Vercel. `; } if (modelId === "muse-glimmer-30b") { diff --git a/packages/core/test/neosantara-sync.test.ts b/packages/core/test/neosantara-sync.test.ts index 9fddb2dd2c3..4aa5f4f6b43 100644 --- a/packages/core/test/neosantara-sync.test.ts +++ b/packages/core/test/neosantara-sync.test.ts @@ -265,6 +265,8 @@ test("toggle models carry a wire-comment header; effort models carry none", () = // minimax-m2.7 documents always-on reasoning. expect(neosantaraReasoningHeader([], "minimax-m2.7")).toContain("Always-on thinking"); + // kimi-k2-thinking documents dedicated thinking variant. + expect(neosantaraReasoningHeader([], "kimi-k2-thinking")).toContain("Dedicated thinking variant"); // muse-glimmer-30b documents external baseline sources. expect(neosantaraReasoningHeader([{ type: "effort", values: ["low"] as never }], "muse-glimmer-30b")).toContain( "https://huggingface.co/meta-models/Muse-Glimmer-30B", diff --git a/providers/neosantara/models/kimi-k2-thinking.toml b/providers/neosantara/models/kimi-k2-thinking.toml index 88d416695f5..2fbdb7e22cc 100644 --- a/providers/neosantara/models/kimi-k2-thinking.toml +++ b/providers/neosantara/models/kimi-k2-thinking.toml @@ -1,3 +1,5 @@ +# Dedicated thinking variant: always-on reasoning with no caller control. +# Matches lab providers/moonshotai/models/kimi-k2-thinking.toml and peers OpenRouter/Vercel. base_model = "moonshotai/kimi-k2-thinking" reasoning_options = [] diff --git a/providers/neosantara/provider.toml b/providers/neosantara/provider.toml index ed72cfd0d22..bcd8aa05c31 100644 --- a/providers/neosantara/provider.toml +++ b/providers/neosantara/provider.toml @@ -12,9 +12,10 @@ # ["none"] -> toggle (reasoning_effort = "none" is off; carries a leading wire comment) # graded set -> effort, using the model's real levels (lab + same-surface peers), never a # full enum. Host ceiling is none|minimal|low|medium|high|xhigh|max. -# When has_toggle is true, toggle is authored alongside graded effort (excluding "none"). -# A reasoning model that does not report a surface is skipped and reported (its controls are -# unknown, not always-on). Capability flags come from the catalog, not lab metadata. +# When off is expressed via reasoning_effort = "none" alongside graded levels, AGENTS.md strictly +# requires authoring only `effort` with `none` in values and NO toggle. Only binary on/off models +# (no graded levels, only ["none"]) author toggle alone with a wire comment. Dedicated headers +# document always-on relays (minimax-m2.7, kimi-k2-thinking) and external citations. # # Streaming: Neosantara normalizes reasoning output across all upstream providers into the # OpenAI-compatible delta.reasoning_content field (see https://docs.neosantara.xyz/en/capability/reasoning).