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..7f25f55e42f --- /dev/null +++ b/packages/core/src/sync/providers/concentrate.ts @@ -0,0 +1,351 @@ +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, + // 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, + // 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, + 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..de85a0f8112 --- /dev/null +++ b/packages/core/test/concentrate.test.ts @@ -0,0 +1,282 @@ +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"); + }); + + test("preserves a hand-authored interleaved field across sync", () => { + const translated = buildConcentrateModel(concentrateModel(), { + 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 { + 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..fa757d9dd42 --- /dev/null +++ b/providers/concentrate/models/claude-opus-4-5.toml @@ -0,0 +1,19 @@ +# 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 + +interleaved = 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..e7364562d55 --- /dev/null +++ b/providers/concentrate/models/claude-sonnet-5.toml @@ -0,0 +1,21 @@ +# 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 + +interleaved = 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..c706adb1e39 --- /dev/null +++ b/providers/concentrate/models/gemini-3.5-flash.toml @@ -0,0 +1,17 @@ +# 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" + +interleaved = true + +[[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..99aaae9e715 --- /dev/null +++ b/providers/concentrate/models/gpt-5.4.toml @@ -0,0 +1,20 @@ +# Pricing, limits and reasoning surface as published for this ID. +# 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"] + +[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..b00ec094c98 --- /dev/null +++ b/providers/concentrate/models/gpt-5.5.toml @@ -0,0 +1,20 @@ +# Pricing, limits and reasoning surface as published for this ID. +# 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"] + +[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..b140c4a2451 --- /dev/null +++ b/providers/concentrate/models/grok-4.5.toml @@ -0,0 +1,28 @@ +# 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" + +interleaved = true + +[[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..cb15f9d29e7 --- /dev/null +++ b/providers/concentrate/models/o3.toml @@ -0,0 +1,14 @@ +# Pricing, limits and reasoning surface as published for this ID. +# https://concentrate.ai/models/o3 +base_model = "openai/o3" + +interleaved = true + +[[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"