From c60597902d013ddc11acd56592cf45d6e342fb22 Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Wed, 19 Aug 2026 17:19:56 +0530 Subject: [PATCH 1/6] feat(concentrate): add Concentrate provider Register Concentrate as an OpenAI-compatible aggregator with an initial set of eight models. The remaining catalog follows in separate batches so each change stays small enough to review. The sync joins the public model list with each model's detail endpoint and derives pricing, limits, modalities, tool calling, structured output and attachment support from that data. Capabilities and limits come from the aggregate list/detail payloads rather than a single upstream route, so a capability-thin route cannot narrow what the gateway accepts, and output limits are capped by canonical models.dev metadata. Three rules encode the gateway's serving reality rather than its catalog: - Pricing defaults to the cheapest advertised route, but IDs whose live traffic is relayed elsewhere are pinned to the observed serving route, so published cost matches what callers are charged. - The gateway-level capability is authoritative for structured output. A route can advertise json_schema while the relay in front of it does not expose the feature. - Context pricing tiers at or above a model's own window are dropped, because such a band can never be entered. Reasoning controls are authored per model from the host's declared route metadata rather than inferred from the gateway's normalization enum, which silently accepts unsupported effort values. Every published effort value is backed by a route that declares it, and narrower or empty sets carry a leading comment with the API evidence. New remote models are reported instead of created automatically so each one gets that review first. --- packages/core/src/sync/index.ts | 4 + .../core/src/sync/providers/concentrate.ts | 338 ++++++++++++++++++ packages/core/test/concentrate.test.ts | 250 +++++++++++++ providers/concentrate/logo.svg | 1 + .../concentrate/models/claude-opus-4-5.toml | 17 + .../concentrate/models/claude-sonnet-5.toml | 19 + .../concentrate/models/deepseek-v3-0324.toml | 16 + .../concentrate/models/gemini-3.5-flash.toml | 15 + providers/concentrate/models/gpt-5.4.toml | 18 + providers/concentrate/models/gpt-5.5.toml | 18 + providers/concentrate/models/grok-4.5.toml | 26 ++ providers/concentrate/models/o3.toml | 12 + providers/concentrate/provider.toml | 5 + 13 files changed, 739 insertions(+) create mode 100644 packages/core/src/sync/providers/concentrate.ts create mode 100644 packages/core/test/concentrate.test.ts create mode 100644 providers/concentrate/logo.svg create mode 100644 providers/concentrate/models/claude-opus-4-5.toml create mode 100644 providers/concentrate/models/claude-sonnet-5.toml create mode 100644 providers/concentrate/models/deepseek-v3-0324.toml create mode 100644 providers/concentrate/models/gemini-3.5-flash.toml create mode 100644 providers/concentrate/models/gpt-5.4.toml create mode 100644 providers/concentrate/models/gpt-5.5.toml create mode 100644 providers/concentrate/models/grok-4.5.toml create mode 100644 providers/concentrate/models/o3.toml create mode 100644 providers/concentrate/provider.toml diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 34691c06c5f..963f7eadf60 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -10,6 +10,7 @@ import { anthropic } from "./providers/anthropic.js"; import { baseten } from "./providers/baseten.js"; import { chutes } from "./providers/chutes.js"; import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js"; +import { concentrate } from "./providers/concentrate.js"; import { cortecs } from "./providers/cortecs.js"; import { crossmodel } from "./providers/crossmodel.js"; import { deepinfra } from "./providers/deepinfra.js"; @@ -119,6 +120,7 @@ export const providers: { baseten: SyncProvider; chutes: SyncProvider; "cloudflare-workers-ai": SyncProvider; + concentrate: SyncProvider; cortecs: SyncProvider; crossmodel: SyncProvider; deepinfra: SyncProvider; @@ -150,6 +152,7 @@ export const providers: { baseten, chutes, "cloudflare-workers-ai": cloudflareWorkersAi, + concentrate, cortecs, crossmodel, deepinfra, @@ -179,6 +182,7 @@ export const providers: { export const groups = { aggregators: [ + "concentrate", "crossmodel", "edenai", "empiriolabs", diff --git a/packages/core/src/sync/providers/concentrate.ts b/packages/core/src/sync/providers/concentrate.ts new file mode 100644 index 00000000000..6f1305ebbac --- /dev/null +++ b/packages/core/src/sync/providers/concentrate.ts @@ -0,0 +1,338 @@ +import { z } from "zod"; + +import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js"; +import { factorBaseModel, modelMetadata, resolveCanonicalBaseModel } from "./openrouter.js"; + +const API_ENDPOINT = "https://api.concentrate.ai/v1/models"; +const DETAIL_CONCURRENCY = 12; + +const Money = z.object({ + price: z.object({ USD: z.number().nonnegative() }).passthrough(), + units: z.number().positive(), +}).passthrough(); + +const TokenCache = z.object({ + read: Money.optional(), + write: z.object({ cache_write_tokens: Money.optional() }).passthrough().optional(), +}).passthrough(); + +const TokenTier = z.object({ + above: z.number().int().nonnegative(), + input: Money, + output: Money, + cache: TokenCache.optional(), +}).passthrough(); + +const TokenPricing = z.object({ + input: Money, + output: Money, + cache: TokenCache.optional(), + tiers: z.array(TokenTier).optional(), +}).passthrough(); + +const InputSupport = z.object({ + text: z.boolean().optional(), + image: z.union([z.boolean(), z.record(z.boolean())]).optional(), + file: z.union([z.boolean(), z.record(z.boolean())]).optional(), +}).passthrough(); + +const ProviderRoute = z.object({ + provider_slug: z.string(), + pricing: z.object({ tokens: TokenPricing }).passthrough(), + context_window: z.number().int().positive(), + max_output_tokens: z.number().int().positive(), + supports: z.object({ + input: InputSupport.optional(), + reasoning: z.object({ effort: z.record(z.boolean()).optional() }).passthrough().optional(), + temperature: z.boolean().optional(), + text: z.object({ + format: z.object({ + json_schema: z.boolean().optional(), + json_object: z.boolean().optional(), + }).passthrough().optional(), + }).passthrough().optional(), + tools: z.object({ function_calling: z.boolean().optional() }).passthrough().optional(), + }).passthrough(), +}).passthrough(); + +const EffortSupport = z.object({ + supported: z.boolean(), + none: z.object({ supported: z.boolean() }).optional(), + minimal: z.object({ supported: z.boolean() }).optional(), + low: z.object({ supported: z.boolean() }).optional(), + medium: z.object({ supported: z.boolean() }).optional(), + high: z.object({ supported: z.boolean() }).optional(), + xhigh: z.object({ supported: z.boolean() }).optional(), + max: z.object({ supported: z.boolean() }).optional(), +}).passthrough(); + +const ConcentrateListModel = z.object({ + id: z.string(), + display_name: z.string(), + owned_by: z.string(), + created_at: z.string(), + max_input_tokens: z.number().int().positive(), + max_tokens: z.number().int().positive(), + capabilities: z.object({ + effort: EffortSupport, + image_input: z.object({ supported: z.boolean() }).passthrough(), + pdf_input: z.object({ supported: z.boolean() }).passthrough(), + structured_outputs: z.object({ supported: z.boolean() }).passthrough(), + thinking: z.object({ + supported: z.boolean(), + types: z.object({ + adaptive: z.object({ supported: z.boolean() }).optional(), + enabled: z.object({ supported: z.boolean() }).optional(), + }).passthrough(), + }).passthrough(), + }).passthrough(), +}).passthrough(); + +const ConcentrateListResponse = z.object({ + data: z.array(ConcentrateListModel), + has_more: z.boolean(), +}).passthrough(); + +const ConcentrateDetail = z.object({ + slug: z.string(), + name: z.string(), + description: z.string(), + release_date: z.number(), + author: z.object({ slug: z.string() }).passthrough(), + providers: z.record(ProviderRoute), +}).passthrough(); + +export const ConcentrateModel = z.object({ + summary: ConcentrateListModel, + detail: ConcentrateDetail, +}); + +const ConcentrateResponse = z.object({ data: z.array(ConcentrateModel) }); + +export type ConcentrateModel = z.infer; +export type ConcentrateRoute = z.infer; +type Fetcher = (input: string | URL | Request, init?: RequestInit) => Promise; + +// Concentrate owner slugs that differ from the canonical metadata prefix. Slugs +// that already match one (anthropic, deepseek, google, openai, xai, moonshot, +// zai) need no entry and fall through unchanged. +const OWNER_PREFIXES: Record = { + mistral: "mistralai", + stepfunai: "stepfun", +}; + +// Concentrate intentionally exposes stable, punctuation-light aliases. Map the +// aliases whose canonical models.dev IDs carry dates, sizes, or lab casing. +const BASE_MODEL_ALIASES: Record = { + "claude-sonnet-4": "anthropic/claude-sonnet-4-20250514", + "gemma-4-26b": "google/gemma-4-26b-a4b-it", + "grok-4.20-non-reasoning": "xai/grok-4.20-0309-non-reasoning", + "deepseek-v3-1": "deepseek/deepseek-v3.1", + "deepseek-v4-flash-0423": "deepseek/deepseek-v4-flash", +}; + +// A few catalog entries advertise routes that do not serve normal traffic, so +// the cheapest advertised price is not what callers are billed. The generic +// GPT-OSS IDs list a $0 Blue Lobster route and sub-cent DeepInfra/Novita routes, +// but live completions on 2026-08-19 were relayed through Fireworks and returned +// that route's non-zero billed cost. Pin the observed serving route for those +// IDs; everything else keeps the cheapest-advertised-route baseline. +const SERVING_ROUTE_OVERRIDES: Record = { + "gpt-oss-120b": "fireworks", + "gpt-oss-20b": "fireworks", +}; + +export const concentrate = { + id: "concentrate", + name: "Concentrate", + modelsDir: "providers/concentrate/models", + // Concentrate normalizes reasoning effort across upstreams and may degrade + // unsupported controls. New files need a lab/peer baseline before publishing + // exact caller-visible reasoning options. + skipCreates: true, + trackMissingModels: false, + deleteMissing: false, + sourceID(model) { + return model.summary.id; + }, + skippedNotice(ids) { + if (ids.length === 0) return []; + return [ + `${ids.length} Concentrate models were not added automatically because relay reasoning controls require per-model review.`, + `Remote IDs not currently curated: ${ids.map((id) => `\`${id}\``).join(", ")}`, + ]; + }, + fetchModels() { + return fetchConcentrateModels(); + }, + parseModels(raw) { + return ConcentrateResponse.parse(raw).data; + }, + translateModel(model, context) { + const translated = buildConcentrateModel(model, context.existing(model.summary.id)); + return translated === undefined ? undefined : { id: model.summary.id, model: translated }; + }, +} satisfies SyncProvider; + +export async function fetchConcentrateModels(fetcher: Fetcher = fetch) { + const response = await fetcher(API_ENDPOINT); + if (!response.ok) { + throw new Error(`Concentrate model list request failed: ${response.status} ${response.statusText}`); + } + const list = ConcentrateListResponse.parse(await response.json()); + if (list.has_more) throw new Error("Concentrate model list unexpectedly returned a partial page"); + + const batches = Array.from( + { length: Math.ceil(list.data.length / DETAIL_CONCURRENCY) }, + (_, index) => list.data.slice(index * DETAIL_CONCURRENCY, (index + 1) * DETAIL_CONCURRENCY), + ); + const data: ConcentrateModel[] = []; + for (const batch of batches) { + data.push(...await Promise.all(batch.map(async (summary) => { + const detailResponse = await fetcher(`${API_ENDPOINT}/${encodeURIComponent(summary.id)}`); + if (!detailResponse.ok) { + throw new Error( + `Concentrate model detail request failed for ${summary.id}: ${detailResponse.status} ${detailResponse.statusText}`, + ); + } + const detail = ConcentrateDetail.parse(await detailResponse.json()); + if (detail.slug !== summary.id) { + throw new Error(`Concentrate model detail ID mismatch: expected ${summary.id}, received ${detail.slug}`); + } + return { summary, detail }; + }))); + } + return { data }; +} + +export function resolveConcentrateBaseModel(model: ConcentrateModel) { + const alias = BASE_MODEL_ALIASES[model.summary.id]; + if (alias !== undefined) return alias; + const prefix = OWNER_PREFIXES[model.summary.owned_by] ?? model.summary.owned_by; + return resolveCanonicalBaseModel(`${prefix}/${model.summary.id}`); +} + +export function selectConcentratePricingRoute(model: ConcentrateModel) { + // Concentrate can serve one model through several upstreams. Its catalog UI + // presents the lowest available token price. Select that route for cost only; + // gateway-wide capabilities and limits come from aggregate list/detail data. + const routes = Object.entries(model.detail.providers).map(([id, route]) => ({ + id, + route, + cost: money(route.pricing.tokens.input) + money(route.pricing.tokens.output), + })); + + // Where a live probe showed which upstream actually serves the ID, bill from + // that route instead of the cheapest advertised one. See SERVING_ROUTE_OVERRIDES. + const pinned = SERVING_ROUTE_OVERRIDES[model.summary.id]; + const served = pinned === undefined ? undefined : routes.find((entry) => entry.id === pinned); + if (served !== undefined) return served; + + return routes.reduce<{ id: string; route: ConcentrateRoute; cost: number } | undefined>( + (best, entry) => (best === undefined || entry.cost < best.cost ? entry : best), + undefined, + ); +} + +export function concentrateSurface( + model: ConcentrateModel, + outputLimit = model.summary.max_tokens, +) { + const routes = Object.values(model.detail.providers); + return { + input: [ + "text" as const, + ...(model.summary.capabilities.image_input.supported + || routes.some((route) => supported(route.supports.input?.image)) + ? ["image" as const] + : []), + ...(model.summary.capabilities.pdf_input.supported + || routes.some((route) => supported(route.supports.input?.file)) + ? ["pdf" as const] + : []), + ], + limit: { + context: model.summary.max_input_tokens, + output: Math.min(model.summary.max_tokens, outputLimit), + }, + tool_call: routes.some((route) => route.supports.tools?.function_calling === true), + // The gateway-level capability is authoritative for structured output: a + // route can advertise json_schema while the relay in front of it does not + // expose the feature. claude-opus-4-1 lists structured_outputs.supported = + // false even though its anthropic route advertises json_schema, and the + // OpenRouter peer for that same base model also publishes false. Trusting + // any permissive route here would overstate what callers can actually use. + structured_output: model.summary.capabilities.structured_outputs.supported, + }; +} + +export function buildConcentrateModel( + model: ConcentrateModel, + existing: ExistingModel | undefined, +): SyncedModel | undefined { + const baseModel = existing?.base_model ?? resolveConcentrateBaseModel(model); + if (baseModel === undefined) return undefined; + const selected = selectConcentratePricingRoute(model); + if (selected === undefined) return undefined; + + const pricingRoute = selected.route; + const surface = concentrateSurface(model, canonicalOutputLimit(baseModel)); + const tokens = pricingRoute.pricing.tokens; + // A context tier only bills once a request exceeds its threshold, so a tier at + // or above the model's own context window can never be entered and would + // advertise a price nobody can be charged. Concentrate publishes exactly that + // for the Claude Sonnet IDs it caps at 200k while carrying Anthropic's + // long-context band at above = 200_000. Drop unreachable tiers rather than + // relaying them. + const tiers = tokens.tiers + ?.filter((tier) => tier.above < surface.limit.context) + .map((tier) => ({ + tier: { type: "context" as const, size: tier.above }, + input: money(tier.input), + output: money(tier.output), + cache_read: optionalMoney(tier.cache?.read), + cache_write: optionalMoney(tier.cache?.write?.cache_write_tokens), + })); + const cost = { + input: money(tokens.input), + output: money(tokens.output), + cache_read: optionalMoney(tokens.cache?.read), + cache_write: optionalMoney(tokens.cache?.write?.cache_write_tokens), + tiers: tiers !== undefined && tiers.length > 0 ? tiers : undefined, + }; + + return factorBaseModel(baseModel, { + attachment: surface.input.some((value) => value !== "text"), + // Concentrate Chat exposes reasoning_effort and reports model-specific + // support per upstream route. Preserve the reviewed lab/peer intersection + // authored in each provider TOML rather than treating the gateway's full + // normalization enum as native support for every model. + // https://concentrate.ai/docs/api-reference/endpoint/chat-completions + // https://concentrate.ai/docs/api-reference/endpoint/get-model + reasoning_options: existing?.reasoning_options, + tool_call: surface.tool_call, + structured_output: surface.structured_output, + cost, + limit: surface.limit, + modalities: { input: surface.input, output: ["text"] }, + }, surface.limit, existing?.base_model_omit); +} + +function canonicalOutputLimit(baseModel: string) { + const limit = modelMetadata(baseModel).limit; + if (limit === null || typeof limit !== "object" || Array.isArray(limit)) return undefined; + return typeof limit.output === "number" ? limit.output : undefined; +} + +function supported(value: boolean | Record | undefined) { + if (typeof value === "boolean") return value; + return value !== undefined && Object.values(value).some(Boolean); +} + +function money(value: z.infer) { + return Math.round((value.price.USD * 1_000_000 / value.units) * 1_000_000) / 1_000_000; +} + +function optionalMoney(value: z.infer | undefined) { + return value === undefined ? undefined : money(value); +} diff --git a/packages/core/test/concentrate.test.ts b/packages/core/test/concentrate.test.ts new file mode 100644 index 00000000000..0bc2f532823 --- /dev/null +++ b/packages/core/test/concentrate.test.ts @@ -0,0 +1,250 @@ +import { describe, expect, test } from "bun:test"; + +import { + buildConcentrateModel, + concentrate, + concentrateSurface, + type ConcentrateModel, + fetchConcentrateModels, + resolveConcentrateBaseModel, + selectConcentratePricingRoute, +} from "../src/sync/providers/concentrate.js"; + +describe("Concentrate sync", () => { + test("preserves the deliberately partial catalog without opening missing-model issues", () => { + expect(concentrate.skipCreates).toBe(true); + expect(concentrate.trackMissingModels).toBe(false); + expect(concentrate.deleteMissing).toBe(false); + }); + + test("fetches and joins the public list with model details", async () => { + const model = concentrateModel(); + const urls: string[] = []; + const result = await fetchConcentrateModels(async (input) => { + const url = String(input); + urls.push(url); + if (url.endsWith("/gpt-5.6-terra")) return Response.json(model.detail); + return Response.json({ data: [model.summary], has_more: false }); + }); + + expect(urls).toEqual([ + "https://api.concentrate.ai/v1/models", + "https://api.concentrate.ai/v1/models/gpt-5.6-terra", + ]); + expect(result.data).toEqual([model]); + }); + + test("uses stable Concentrate aliases for canonical model metadata", () => { + expect(resolveConcentrateBaseModel(concentrateModel({ id: "claude-sonnet-4", owner: "anthropic" }))) + .toBe("anthropic/claude-sonnet-4-20250514"); + expect(resolveConcentrateBaseModel(concentrateModel({ id: "gpt-5.6-terra", owner: "openai" }))) + .toBe("openai/gpt-5.6-terra"); + }); + + test("uses the cheapest route only for pricing and preserves curated reasoning controls", () => { + const model = concentrateModel(); + const selected = selectConcentratePricingRoute(model); + expect(selected?.id).toBe("openai"); + expect(selected?.route.context_window).toBe(1_050_000); + + const reasoningOptions = [{ type: "effort" as const, values: ["low" as const, "high" as const] }]; + const translated = buildConcentrateModel(model, { reasoning_options: reasoningOptions }); + expect(translated).toMatchObject({ + base_model: "openai/gpt-5.6-terra", + reasoning_options: reasoningOptions, + cost: { + input: 2, + output: 12, + cache_read: 0.2, + cache_write: 2.5, + tiers: [{ + tier: { type: "context", size: 272_000 }, + input: 4, + output: 18, + }], + }, + }); + expect(translated).not.toHaveProperty("reasoning"); + expect(translated).not.toHaveProperty("temperature"); + }); + + test("uses aggregate limits and capabilities when the cheapest route is capability-thin", () => { + const model = concentrateModel(); + model.detail.providers.free = { + ...model.detail.providers.openai, + provider_slug: "free", + pricing: { + tokens: { + input: { price: { USD: 0 }, units: 1_000_000 }, + output: { price: { USD: 0 }, units: 1_000_000 }, + }, + }, + context_window: 16_384, + max_output_tokens: 4_096, + supports: { + input: { text: true }, + tools: { function_calling: false }, + }, + }; + + const translated = buildConcentrateModel(model, undefined); + expect(selectConcentratePricingRoute(model)?.id).toBe("free"); + expect(translated).toMatchObject({ cost: { input: 0, output: 0 } }); + expect(concentrateSurface(model)).toEqual({ + input: ["text", "image", "pdf"], + limit: { context: 1_050_000, output: 128_000 }, + tool_call: true, + structured_output: true, + }); + }); + + test("does not raise output above the canonical model ceiling", () => { + const model = concentrateModel(); + model.summary.max_tokens = 1_048_576; + + const translated = buildConcentrateModel(model, { + base_model: "deepseek/deepseek-v4-pro", + }); + expect(translated).not.toMatchObject({ + limit: { output: 1_048_576 }, + }); + expect(concentrateSurface(model, 384_000)).toMatchObject({ + limit: { output: 384_000 }, + }); + }); + + test("bills GPT-OSS from the Fireworks route that actually serves the relay", () => { + // gpt-oss-120b advertises a $0 Blue Lobster route and a sub-cent DeepInfra + // route, but live requests relay through Fireworks and are billed there. + const model = concentrateModel({ id: "gpt-oss-120b" }); + const priced = (input: number, output: number, slug: string) => ({ + ...model.detail.providers.openai!, + provider_slug: slug, + pricing: { + tokens: { + input: { price: { USD: input }, units: 1_000_000 }, + output: { price: { USD: output }, units: 1_000_000 }, + cache: { read: { price: { USD: 0.015 }, units: 1_000_000 } }, + }, + }, + }); + model.detail.providers.bluelobster = priced(0, 0, "bluelobster"); + model.detail.providers.deepinfra = priced(0.037, 0.17, "deepinfra"); + model.detail.providers.fireworks = priced(0.15, 0.6, "fireworks"); + + // Neither the $0 route nor the cheapest billable route may win here. + expect(selectConcentratePricingRoute(model)?.id).toBe("fireworks"); + expect(buildConcentrateModel(model, { base_model: "openai/gpt-oss-120b" })) + .toMatchObject({ cost: { input: 0.15, output: 0.6, cache_read: 0.015 } }); + }); + + test("drops context tiers that sit at or above the model's own window", () => { + // Concentrate carries Anthropic's above=200_000 band on IDs it caps at a + // 200k window, so the tier can never be entered. + const model = concentrateModel({ id: "claude-sonnet-4-5", owner: "anthropic" }); + model.summary.max_input_tokens = 200_000; + for (const route of Object.values(model.detail.providers)) route.context_window = 200_000; + + const translated = buildConcentrateModel(model, { base_model: "anthropic/claude-sonnet-4-5" }); + expect(translated?.cost).not.toHaveProperty("tiers", expect.anything()); + expect(translated?.cost.tiers).toBeUndefined(); + + // A tier below the window is still published. + model.summary.max_input_tokens = 1_000_000; + expect(buildConcentrateModel(model, { base_model: "anthropic/claude-sonnet-4-5" })?.cost.tiers) + .toMatchObject([{ tier: { type: "context", size: 272_000 } }]); + }); + + test("does not let a permissive route overstate structured output", () => { + // claude-opus-4-1 reports structured_outputs.supported = false while its + // anthropic route still advertises json_schema; the relay does not expose it. + const model = concentrateModel({ id: "claude-opus-4-1", owner: "anthropic" }); + model.summary.capabilities.structured_outputs = { supported: false }; + expect(model.detail.providers.openai?.supports.text?.format?.json_schema).toBe(true); + + expect(concentrateSurface(model).structured_output).toBe(false); + expect(buildConcentrateModel(model, { base_model: "anthropic/claude-opus-4-1" })) + .toMatchObject({ structured_output: false }); + }); + + test("falls back to the cheapest advertised route when the pinned route is absent", () => { + const model = concentrateModel({ id: "gpt-oss-120b" }); + expect(model.detail.providers.fireworks).toBeUndefined(); + expect(selectConcentratePricingRoute(model)?.id).toBe("openai"); + }); + + test("does not invent reasoning controls from Concentrate's normalization enum", () => { + const translated = buildConcentrateModel(concentrateModel(), undefined); + expect(translated).not.toHaveProperty("reasoning_options"); + }); +}); + +function concentrateModel(overrides: { id?: string; owner?: string } = {}): ConcentrateModel { + const id = overrides.id ?? "gpt-5.6-terra"; + const owner = overrides.owner ?? "openai"; + const money = (price: number) => ({ price: { USD: price }, units: 1_000_000 }); + const route = (input: number, output: number) => ({ + provider_slug: "openai", + pricing: { + tokens: { + input: money(input), + output: money(output), + cache: { + read: money(0.2), + write: { cache_write_tokens: money(2.5) }, + }, + tiers: [{ + above: 272_000, + input: money(4), + output: money(18), + }], + }, + }, + context_window: 1_050_000, + max_output_tokens: 128_000, + supports: { + input: { text: true, image: { png: true }, file: { pdf: true } }, + reasoning: { effort: { none: true, low: true, high: true } }, + temperature: false, + text: { format: { json_schema: true } }, + tools: { function_calling: true }, + }, + }); + + return { + summary: { + id, + display_name: "GPT 5.6 Terra", + owned_by: owner, + created_at: "2026-07-09T00:00:00.000Z", + max_input_tokens: 1_050_000, + max_tokens: 128_000, + capabilities: { + effort: { + supported: true, + none: { supported: true }, + low: { supported: true }, + high: { supported: true }, + }, + image_input: { supported: true }, + pdf_input: { supported: true }, + structured_outputs: { supported: true }, + thinking: { + supported: true, + types: { enabled: { supported: true } }, + }, + }, + }, + detail: { + slug: id, + name: "GPT 5.6 Terra", + description: "Cost-optimized GPT model", + release_date: 1_783_555_200_000, + author: { slug: owner }, + providers: { + azure: { ...route(3, 15), provider_slug: "azure" }, + openai: route(2, 12), + }, + }, + }; +} diff --git a/providers/concentrate/logo.svg b/providers/concentrate/logo.svg new file mode 100644 index 00000000000..33f748d93f8 --- /dev/null +++ b/providers/concentrate/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/providers/concentrate/models/claude-opus-4-5.toml b/providers/concentrate/models/claude-opus-4-5.toml new file mode 100644 index 00000000000..5e811c702af --- /dev/null +++ b/providers/concentrate/models/claude-opus-4-5.toml @@ -0,0 +1,17 @@ +# Pricing, limits and reasoning surface as published for this ID. +# Concentrate exposes the off switch as `reasoning_effort: none` rather than a +# separate toggle, and every route declares it. Live probes on 2026-08-19 with an +# identical prompt used 970 completion tokens at none against 2132 at high, so it +# genuinely reduces thinking rather than being normalized away. +# https://concentrate.ai/models/claude-opus-4-5 +base_model = "anthropic/claude-opus-4-5" +structured_output = true + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 5 +output = 25 +cache_read = 0.5 diff --git a/providers/concentrate/models/claude-sonnet-5.toml b/providers/concentrate/models/claude-sonnet-5.toml new file mode 100644 index 00000000000..ac3c8cd23be --- /dev/null +++ b/providers/concentrate/models/claude-sonnet-5.toml @@ -0,0 +1,19 @@ +# Concentrate exposes the off switch as `reasoning_effort: none` rather than a +# separate toggle; all three routes (anthropic, azure, bedrock) declare it. This +# ID returns no reasoning text, so effort was verified by output budget instead: +# live probes on 2026-08-19 with an identical prompt produced 2_006 completion +# tokens at none, 2_328 at low, 4_799 at high and 16_000 at max, so the tiers +# have distinct effect. `max` is declared by the anthropic and azure routes; the +# bedrock route stops at xhigh. +# https://concentrate.ai/models/claude-sonnet-5 +base_model = "anthropic/claude-sonnet-5" +structured_output = true + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh", "max"] + +[cost] +input = 2 +output = 10 +cache_read = 0.2 diff --git a/providers/concentrate/models/deepseek-v3-0324.toml b/providers/concentrate/models/deepseek-v3-0324.toml new file mode 100644 index 00000000000..e9c22eafcec --- /dev/null +++ b/providers/concentrate/models/deepseek-v3-0324.toml @@ -0,0 +1,16 @@ +# PDF input is a genuine host delta rather than an inherited lab capability: +# Concentrate reports pdf_input.supported = true for this ID and its deepinfra +# route accepts file.pdf, while the crusoe route does not. Image input stays off, +# matching the lab surface. +# https://concentrate.ai/models/deepseek-v3-0324 +base_model = "deepseek/deepseek-v3-0324" +attachment = true +structured_output = true + +[cost] +input = 0.24 +output = 0.9 +cache_read = 0.135 + +[modalities] +input = ["text", "pdf"] diff --git a/providers/concentrate/models/gemini-3.5-flash.toml b/providers/concentrate/models/gemini-3.5-flash.toml new file mode 100644 index 00000000000..04fbeaa8a25 --- /dev/null +++ b/providers/concentrate/models/gemini-3.5-flash.toml @@ -0,0 +1,15 @@ +# Pricing, limits and reasoning surface as published for this ID. +# https://concentrate.ai/models/gemini-3.5-flash +base_model = "google/gemini-3.5-flash" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] + +[cost] +input = 1.5 +output = 9 +cache_read = 0.15 + +[modalities] +input = ["text", "image", "pdf"] diff --git a/providers/concentrate/models/gpt-5.4.toml b/providers/concentrate/models/gpt-5.4.toml new file mode 100644 index 00000000000..87a1ac5f435 --- /dev/null +++ b/providers/concentrate/models/gpt-5.4.toml @@ -0,0 +1,18 @@ +# Pricing, limits and reasoning surface as published for this ID. +# https://concentrate.ai/models/gpt-5.4 +base_model = "openai/gpt-5.4" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] + +[cost] +input = 2.5 +output = 15 +cache_read = 0.25 + +[[cost.tiers]] +tier = { type = "context", size = 272_000 } +input = 5 +output = 22.5 +cache_read = 0.5 diff --git a/providers/concentrate/models/gpt-5.5.toml b/providers/concentrate/models/gpt-5.5.toml new file mode 100644 index 00000000000..bf939d5eaa0 --- /dev/null +++ b/providers/concentrate/models/gpt-5.5.toml @@ -0,0 +1,18 @@ +# Pricing, limits and reasoning surface as published for this ID. +# https://concentrate.ai/models/gpt-5.5 +base_model = "openai/gpt-5.5" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh"] + +[cost] +input = 5 +output = 30 +cache_read = 0.5 + +[[cost.tiers]] +tier = { type = "context", size = 272_000 } +input = 10 +output = 45 +cache_read = 1 diff --git a/providers/concentrate/models/grok-4.5.toml b/providers/concentrate/models/grok-4.5.toml new file mode 100644 index 00000000000..ac92f9d03bf --- /dev/null +++ b/providers/concentrate/models/grok-4.5.toml @@ -0,0 +1,26 @@ +# Cache reads bill at $0.50/M on this gateway, above the $0.30 first-party rate; +# this is the price the xai route publishes here, and the 200k tier doubles it to +# $1.00/M in line with the tier's input and output rates. +# Effort stays at the lab and peer surface of low|medium|high. The route also +# reports `minimal` and the list capabilities report `max`, but neither is +# published: live probes on 2026-08-19 returned 334 reasoning characters at +# minimal against 355 at low, and 489 at max against 673 at high, so the gateway +# normalizes them rather than honouring a distinct level. `xhigh` behaves the same +# way here (539 characters against 673 at high) and is the Grok 4.6 surface. +# https://concentrate.ai/models/grok-4.5 +base_model = "xai/grok-4.5" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 2 +output = 6 +cache_read = 0.5 + +[[cost.tiers]] +tier = { type = "context", size = 200_000 } +input = 4 +output = 12 +cache_read = 1 diff --git a/providers/concentrate/models/o3.toml b/providers/concentrate/models/o3.toml new file mode 100644 index 00000000000..324fd83cae1 --- /dev/null +++ b/providers/concentrate/models/o3.toml @@ -0,0 +1,12 @@ +# Pricing, limits and reasoning surface as published for this ID. +# https://concentrate.ai/models/o3 +base_model = "openai/o3" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 2 +output = 8 +cache_read = 0.5 diff --git a/providers/concentrate/provider.toml b/providers/concentrate/provider.toml new file mode 100644 index 00000000000..968d967d041 --- /dev/null +++ b/providers/concentrate/provider.toml @@ -0,0 +1,5 @@ +name = "Concentrate" +env = ["CONCENTRATE_API_KEY"] +npm = "@ai-sdk/openai-compatible" +api = "https://api.concentrate.ai/v1" +doc = "https://concentrate.ai/docs" From f9bcb31d01fffe41240c0116a8d5509a25000575 Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Thu, 20 Aug 2026 19:20:07 +0530 Subject: [PATCH 2/6] fix(concentrate): logo dots should render white, not default black fill="currentColor" was resolving to black in most contexts since nothing in this SVG's usage sets a text color to inherit. Concentrate's actual mark uses white dots; switched to an explicit fill="#ffffff" while keeping the background transparent, matching every other provider's backgroundless-icon convention on this site. --- providers/concentrate/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/concentrate/logo.svg b/providers/concentrate/logo.svg index 33f748d93f8..60cbd11e3e4 100644 --- a/providers/concentrate/logo.svg +++ b/providers/concentrate/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 871283097505dd6166f9db69a4db9c2bc96c6308 Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Thu, 20 Aug 2026 19:57:09 +0530 Subject: [PATCH 3/6] fix(concentrate): restore fill=currentColor per the logo CI check The repo's automated PR check flagged the previous commit: provider logos must use currentColor with no hardcoded colors, since a fixed fill breaks theming and can disappear on light backgrounds. Reverting to currentColor - this is the site's own enforced convention, not just an inferred one, so the earlier white-dots-with-transparent-bg attempt was wrong regardless of what the underlying brand mark looks like. --- providers/concentrate/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/concentrate/logo.svg b/providers/concentrate/logo.svg index 60cbd11e3e4..33f748d93f8 100644 --- a/providers/concentrate/logo.svg +++ b/providers/concentrate/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From d0ebf1e5e6ce0b5b402a23b6690d961cb76b9b35 Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Thu, 20 Aug 2026 20:37:40 +0530 Subject: [PATCH 4/6] fix(concentrate): set interleaved.field for all reasoning-capable models rekram1-node flagged that none of the reasoning models had this set. Concentrate's own API docs (checked the live OpenAPI spec) show the message object returns both `reasoning` and `reasoning_content`, with reasoning_content documented as 'included for compatibility - reasoning will be returned rather than reasoning_content'. So Concentrate's real field is `reasoning`, which isn't a valid enum value for interleaved.field (only reasoning_content/reasoning_details are). Checked @ai-sdk/openai-compatible's actual source to make sure reasoning_content is still the right call: it already hardcodes `message.reasoning_content ?? message.reasoning` (and the streaming equivalent) unconditionally, so it picks up Concentrate's `reasoning` field via that fallback regardless of what's set here. reasoning_content is correct - deepseek-v3-0324 correctly left unset since it has no reasoning_options. --- providers/concentrate/models/claude-opus-4-5.toml | 3 +++ providers/concentrate/models/claude-sonnet-5.toml | 3 +++ providers/concentrate/models/gemini-3.5-flash.toml | 3 +++ providers/concentrate/models/gpt-5.4.toml | 3 +++ providers/concentrate/models/gpt-5.5.toml | 3 +++ providers/concentrate/models/grok-4.5.toml | 3 +++ providers/concentrate/models/o3.toml | 3 +++ 7 files changed, 21 insertions(+) diff --git a/providers/concentrate/models/claude-opus-4-5.toml b/providers/concentrate/models/claude-opus-4-5.toml index 5e811c702af..3ea3126b546 100644 --- a/providers/concentrate/models/claude-opus-4-5.toml +++ b/providers/concentrate/models/claude-opus-4-5.toml @@ -15,3 +15,6 @@ values = ["none", "low", "medium", "high"] input = 5 output = 25 cache_read = 0.5 + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/claude-sonnet-5.toml b/providers/concentrate/models/claude-sonnet-5.toml index ac3c8cd23be..32a07f0406b 100644 --- a/providers/concentrate/models/claude-sonnet-5.toml +++ b/providers/concentrate/models/claude-sonnet-5.toml @@ -17,3 +17,6 @@ values = ["none", "low", "medium", "high", "xhigh", "max"] input = 2 output = 10 cache_read = 0.2 + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/gemini-3.5-flash.toml b/providers/concentrate/models/gemini-3.5-flash.toml index 04fbeaa8a25..65cc1f94c7a 100644 --- a/providers/concentrate/models/gemini-3.5-flash.toml +++ b/providers/concentrate/models/gemini-3.5-flash.toml @@ -13,3 +13,6 @@ cache_read = 0.15 [modalities] input = ["text", "image", "pdf"] + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/gpt-5.4.toml b/providers/concentrate/models/gpt-5.4.toml index 87a1ac5f435..3a94ca5cdd8 100644 --- a/providers/concentrate/models/gpt-5.4.toml +++ b/providers/concentrate/models/gpt-5.4.toml @@ -16,3 +16,6 @@ tier = { type = "context", size = 272_000 } input = 5 output = 22.5 cache_read = 0.5 + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/gpt-5.5.toml b/providers/concentrate/models/gpt-5.5.toml index bf939d5eaa0..514ff4b3d12 100644 --- a/providers/concentrate/models/gpt-5.5.toml +++ b/providers/concentrate/models/gpt-5.5.toml @@ -16,3 +16,6 @@ tier = { type = "context", size = 272_000 } input = 10 output = 45 cache_read = 1 + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/grok-4.5.toml b/providers/concentrate/models/grok-4.5.toml index ac92f9d03bf..2ce8ad92b68 100644 --- a/providers/concentrate/models/grok-4.5.toml +++ b/providers/concentrate/models/grok-4.5.toml @@ -24,3 +24,6 @@ tier = { type = "context", size = 200_000 } input = 4 output = 12 cache_read = 1 + +[interleaved] +field = "reasoning_content" diff --git a/providers/concentrate/models/o3.toml b/providers/concentrate/models/o3.toml index 324fd83cae1..99604c8fe60 100644 --- a/providers/concentrate/models/o3.toml +++ b/providers/concentrate/models/o3.toml @@ -10,3 +10,6 @@ values = ["low", "medium", "high"] input = 2 output = 8 cache_read = 0.5 + +[interleaved] +field = "reasoning_content" From f625d5e77bc20880493eb4155c5bc13b75b8b2ef Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Thu, 20 Aug 2026 20:50:21 +0530 Subject: [PATCH 5/6] fix(concentrate): preserve hand-authored interleaved field across sync The bot flagged that buildConcentrateModel passes reasoning_options through from the existing TOML but never did the same for interleaved, so the next bun models:sync concentrate would silently wipe the [interleaved] blocks just added to the 7 reasoning models. Same pattern other aggregators (e.g. OpenRouter) already use for provider-only fields the API doesn't report. Added interleaved: existing?.interleaved next to reasoning_options in the factorBaseModel overrides, plus a positive/negative test pair in concentrate.test.ts mirroring the existing reasoning_options tests. Verified the new positive test actually catches the regression: reverted the source line, confirmed it goes red, restored it, confirmed green. Full package suite: 188 pass, 2 pre-existing failures (DeepInfra-related, confirmed present on the unmodified branch too, unrelated to this). --- packages/core/src/sync/providers/concentrate.ts | 6 ++++++ packages/core/test/concentrate.test.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/core/src/sync/providers/concentrate.ts b/packages/core/src/sync/providers/concentrate.ts index 6f1305ebbac..9dfe350f559 100644 --- a/packages/core/src/sync/providers/concentrate.ts +++ b/packages/core/src/sync/providers/concentrate.ts @@ -310,6 +310,12 @@ export function buildConcentrateModel( // https://concentrate.ai/docs/api-reference/endpoint/chat-completions // https://concentrate.ai/docs/api-reference/endpoint/get-model reasoning_options: existing?.reasoning_options, + // interleaved isn't reported by the API either — it's a hand-authored + // reading of which field (reasoning_content vs reasoning_details) a + // model's route actually streams reasoning through. Preserve it the + // same way reasoning_options is preserved above, or the next sync drops + // every model's [interleaved] block silently. + interleaved: existing?.interleaved, tool_call: surface.tool_call, structured_output: surface.structured_output, cost, diff --git a/packages/core/test/concentrate.test.ts b/packages/core/test/concentrate.test.ts index 0bc2f532823..89cadf3e968 100644 --- a/packages/core/test/concentrate.test.ts +++ b/packages/core/test/concentrate.test.ts @@ -177,6 +177,20 @@ describe("Concentrate sync", () => { const translated = buildConcentrateModel(concentrateModel(), undefined); expect(translated).not.toHaveProperty("reasoning_options"); }); + + test("preserves a hand-authored interleaved field across sync", () => { + const translated = buildConcentrateModel(concentrateModel(), { + interleaved: { field: "reasoning_content" }, + }); + expect(translated).toMatchObject({ + interleaved: { field: "reasoning_content" }, + }); + }); + + test("does not invent an interleaved field when none is authored", () => { + const translated = buildConcentrateModel(concentrateModel(), undefined); + expect(translated).not.toHaveProperty("interleaved"); + }); }); function concentrateModel(overrides: { id?: string; owner?: string } = {}): ConcentrateModel { From 6637eb0c7a5c35e72d106bad41608a379e4ba639 Mon Sep 17 00:00:00 2001 From: AjayK47 Date: Thu, 20 Aug 2026 21:34:07 +0530 Subject: [PATCH 6/6] fix(concentrate): correct interleaved value and preserve more hand-authored fields Two things from review: 1. interleaved was set to { field = "reasoning_content" } on all 7 reasoning models, but our own prior commit documented that Concentrate's API actually returns reasoning on the `reasoning" key, not `reasoning_content` (which is compatibility-only and never populated). That mislabels the wire key for any catalog consumer that doesn't apply the AI SDK's reasoning_content ?? reasoning fallback. Switched to bare interleaved = true, the correct declaration when a model has a real reasoning side channel that isn't one of the two named enum options. Also moved it to match the generator's canonical position (before [[reasoning_options]]) instead of appended at the end of the file. 2. buildConcentrateModel only preserved reasoning_options across syncs, not status/provider/experimental - other ModelBase fields the API doesn't own either. openrouter.ts and nano-gpt.ts already preserve all of these for the same reason: a curator hand-setting one (e.g. marking a model deprecated) would otherwise get silently wiped on the next sync. None of our 8 models currently use these three, so this is preventive, not fixing an active loss - closing the same gap class before it recurs. Test changes: updated the interleaved test to assert true instead of the old field-object shape, added a positive/negative pair for status/provider/experimental mirroring the existing pattern. Verified both new tests actually catch the regression (reverted the source lines, confirmed red, restored, confirmed green) rather than just asserting they pass. Also ran bun run validate (the real zod schema, not just TOML parsing) - exit 0, and inspected the validated o3 output directly to confirm interleaved: true survives validation. Full suite: 190 pass, 2 pre-existing DeepInfra failures unrelated to this and confirmed present on the unmodified branch. --- .../core/src/sync/providers/concentrate.ts | 7 +++++ packages/core/test/concentrate.test.ts | 26 ++++++++++++++++--- .../concentrate/models/claude-opus-4-5.toml | 5 ++-- .../concentrate/models/claude-sonnet-5.toml | 5 ++-- .../concentrate/models/gemini-3.5-flash.toml | 5 ++-- providers/concentrate/models/gpt-5.4.toml | 5 ++-- providers/concentrate/models/gpt-5.5.toml | 5 ++-- providers/concentrate/models/grok-4.5.toml | 5 ++-- providers/concentrate/models/o3.toml | 5 ++-- 9 files changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/core/src/sync/providers/concentrate.ts b/packages/core/src/sync/providers/concentrate.ts index 9dfe350f559..7f25f55e42f 100644 --- a/packages/core/src/sync/providers/concentrate.ts +++ b/packages/core/src/sync/providers/concentrate.ts @@ -316,6 +316,13 @@ export function buildConcentrateModel( // same way reasoning_options is preserved above, or the next sync drops // every model's [interleaved] block silently. interleaved: existing?.interleaved, + // status/provider/experimental are never derived from Concentrate's + // catalog either — same peer pattern as openrouter.ts/nano-gpt.ts, so a + // curator marking a model deprecated, or adding a per-model request + // override, survives the next sync instead of being silently dropped. + status: existing?.status, + provider: existing?.provider, + experimental: existing?.experimental, tool_call: surface.tool_call, structured_output: surface.structured_output, cost, diff --git a/packages/core/test/concentrate.test.ts b/packages/core/test/concentrate.test.ts index 89cadf3e968..de85a0f8112 100644 --- a/packages/core/test/concentrate.test.ts +++ b/packages/core/test/concentrate.test.ts @@ -180,17 +180,35 @@ describe("Concentrate sync", () => { test("preserves a hand-authored interleaved field across sync", () => { const translated = buildConcentrateModel(concentrateModel(), { - interleaved: { field: "reasoning_content" }, - }); - expect(translated).toMatchObject({ - interleaved: { field: "reasoning_content" }, + interleaved: true, }); + expect(translated).toMatchObject({ interleaved: true }); }); test("does not invent an interleaved field when none is authored", () => { const translated = buildConcentrateModel(concentrateModel(), undefined); expect(translated).not.toHaveProperty("interleaved"); }); + + test("preserves hand-authored status/provider/experimental across sync", () => { + const translated = buildConcentrateModel(concentrateModel(), { + status: "deprecated", + provider: { npm: "@ai-sdk/openai-compatible", shape: "completions" }, + experimental: { modes: { fast: { cost: { input: 1, output: 2 } } } }, + }); + expect(translated).toMatchObject({ + status: "deprecated", + provider: { npm: "@ai-sdk/openai-compatible", shape: "completions" }, + experimental: { modes: { fast: { cost: { input: 1, output: 2 } } } }, + }); + }); + + test("does not invent status/provider/experimental when none is authored", () => { + const translated = buildConcentrateModel(concentrateModel(), undefined); + expect(translated).not.toHaveProperty("status"); + expect(translated).not.toHaveProperty("provider"); + expect(translated).not.toHaveProperty("experimental"); + }); }); function concentrateModel(overrides: { id?: string; owner?: string } = {}): ConcentrateModel { diff --git a/providers/concentrate/models/claude-opus-4-5.toml b/providers/concentrate/models/claude-opus-4-5.toml index 3ea3126b546..fa757d9dd42 100644 --- a/providers/concentrate/models/claude-opus-4-5.toml +++ b/providers/concentrate/models/claude-opus-4-5.toml @@ -7,6 +7,8 @@ base_model = "anthropic/claude-opus-4-5" structured_output = true +interleaved = true + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high"] @@ -15,6 +17,3 @@ values = ["none", "low", "medium", "high"] input = 5 output = 25 cache_read = 0.5 - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/claude-sonnet-5.toml b/providers/concentrate/models/claude-sonnet-5.toml index 32a07f0406b..e7364562d55 100644 --- a/providers/concentrate/models/claude-sonnet-5.toml +++ b/providers/concentrate/models/claude-sonnet-5.toml @@ -9,6 +9,8 @@ base_model = "anthropic/claude-sonnet-5" structured_output = true +interleaved = true + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh", "max"] @@ -17,6 +19,3 @@ values = ["none", "low", "medium", "high", "xhigh", "max"] input = 2 output = 10 cache_read = 0.2 - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/gemini-3.5-flash.toml b/providers/concentrate/models/gemini-3.5-flash.toml index 65cc1f94c7a..c706adb1e39 100644 --- a/providers/concentrate/models/gemini-3.5-flash.toml +++ b/providers/concentrate/models/gemini-3.5-flash.toml @@ -2,6 +2,8 @@ # https://concentrate.ai/models/gemini-3.5-flash base_model = "google/gemini-3.5-flash" +interleaved = true + [[reasoning_options]] type = "effort" values = ["minimal", "low", "medium", "high"] @@ -13,6 +15,3 @@ cache_read = 0.15 [modalities] input = ["text", "image", "pdf"] - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/gpt-5.4.toml b/providers/concentrate/models/gpt-5.4.toml index 3a94ca5cdd8..99aaae9e715 100644 --- a/providers/concentrate/models/gpt-5.4.toml +++ b/providers/concentrate/models/gpt-5.4.toml @@ -2,6 +2,8 @@ # https://concentrate.ai/models/gpt-5.4 base_model = "openai/gpt-5.4" +interleaved = true + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] @@ -16,6 +18,3 @@ tier = { type = "context", size = 272_000 } input = 5 output = 22.5 cache_read = 0.5 - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/gpt-5.5.toml b/providers/concentrate/models/gpt-5.5.toml index 514ff4b3d12..b00ec094c98 100644 --- a/providers/concentrate/models/gpt-5.5.toml +++ b/providers/concentrate/models/gpt-5.5.toml @@ -2,6 +2,8 @@ # https://concentrate.ai/models/gpt-5.5 base_model = "openai/gpt-5.5" +interleaved = true + [[reasoning_options]] type = "effort" values = ["none", "low", "medium", "high", "xhigh"] @@ -16,6 +18,3 @@ tier = { type = "context", size = 272_000 } input = 10 output = 45 cache_read = 1 - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/grok-4.5.toml b/providers/concentrate/models/grok-4.5.toml index 2ce8ad92b68..b140c4a2451 100644 --- a/providers/concentrate/models/grok-4.5.toml +++ b/providers/concentrate/models/grok-4.5.toml @@ -10,6 +10,8 @@ # https://concentrate.ai/models/grok-4.5 base_model = "xai/grok-4.5" +interleaved = true + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] @@ -24,6 +26,3 @@ tier = { type = "context", size = 200_000 } input = 4 output = 12 cache_read = 1 - -[interleaved] -field = "reasoning_content" diff --git a/providers/concentrate/models/o3.toml b/providers/concentrate/models/o3.toml index 99604c8fe60..cb15f9d29e7 100644 --- a/providers/concentrate/models/o3.toml +++ b/providers/concentrate/models/o3.toml @@ -2,6 +2,8 @@ # https://concentrate.ai/models/o3 base_model = "openai/o3" +interleaved = true + [[reasoning_options]] type = "effort" values = ["low", "medium", "high"] @@ -10,6 +12,3 @@ values = ["low", "medium", "high"] input = 2 output = 8 cache_read = 0.5 - -[interleaved] -field = "reasoning_content"