diff --git a/package.json b/package.json index 099cbc05e8b..26768f8a453 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test": "bun test", "validate": "bun ./packages/core/script/validate.ts", "compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts", + "aimlapi:sync": "bun ./packages/core/script/sync-models.ts aimlapi", "anthropic:sync": "bun ./packages/core/script/sync-models.ts anthropic", "baseten:sync": "bun ./packages/core/script/sync-models.ts baseten", "deepinfra:sync": "bun ./packages/core/script/sync-models.ts deepinfra", diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 5095f4124ef..3f92fb879ca 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -7,6 +7,7 @@ import { AuthoredModel, AuthoredModelShape, ModelMetadata } from "../schema.js"; import { openMissingModelIssues } from "./missing-issues.js"; import { MissingReasoningOptionsError } from "./missing-reasoning-options.js"; import { aiand } from "./providers/aiand.js"; +import { aimlapi } from "./providers/aimlapi.js"; import { ambient } from "./providers/ambient.js"; import { anthropic } from "./providers/anthropic.js"; import { baseten } from "./providers/baseten.js"; @@ -141,6 +142,7 @@ export interface SyncResult { export const providers: { aiand: SyncProvider; + aimlapi: SyncProvider; ambient: SyncProvider; anthropic: SyncProvider; baseten: SyncProvider; @@ -180,6 +182,7 @@ export const providers: { xai: SyncProvider; } = { aiand, + aimlapi, ambient, anthropic, baseten, @@ -221,6 +224,7 @@ export const providers: { export const groups = { aggregators: [ + "aimlapi", "crossmodel", "edenai", "empiriolabs", diff --git a/packages/core/src/sync/providers/aimlapi.ts b/packages/core/src/sync/providers/aimlapi.ts new file mode 100755 index 00000000000..feb59508973 --- /dev/null +++ b/packages/core/src/sync/providers/aimlapi.ts @@ -0,0 +1,1158 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { z } from "zod"; + +import type { SyncProvider } from "../index.js"; +import { + factorBaseModel, + modelMetadata, + openrouterDirsForMetadataLab, + providerDirForMetadataLab, + resolveModelMetadataBaseModel, +} from "./openrouter.js"; + +// The public catalog needs no key, and `include` is what turns on the pricing +// and modality blocks this sync depends on. +const API_ENDPOINT = "https://api.aimlapi.com/v1/models?include=pricing,modalities"; + +// Per-model request schema. It is the only place the API states which reasoning +// controls a model actually accepts, so reasoning_options is read from here +// rather than assumed. +const DOCS_ENDPOINT = "https://api.aimlapi.com/docs-json"; + +// AI/ML API serves one id under several endpoint types — a model can be both a +// chat model and, say, an image model. Only the chat surface belongs here. +const CHAT_COMPLETIONS_TYPE = "openai/chat-completions"; + +// Values this schema accepts for an "effort" reasoning control, in the order a +// reader expects to see them. Anything the API documents outside this set is +// dropped rather than coerced. +const EFFORT_VALUES = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] as const; +type EffortValue = (typeof EFFORT_VALUES)[number]; +const EFFORT_RANK = new Map(EFFORT_VALUES.map((value, index) => [value as string, index])); +const isEffortValue = (value: string): value is EffortValue => EFFORT_RANK.has(value); + +/** + * Models whose documented schema and whose live behaviour disagree, with what + * the endpoint actually served when asked. + * + * `/docs-json` describes a request *shape* per fallback hop, and a hop can + * advertise a control the vendor behind it still refuses. Reading the schema + * alone therefore over-states these two, and reading only the first hop + * under-states others — neither direction is safe to publish unchecked. + * + * How these were established, because the method is the evidence: each value + * was sent to production with `max_tokens` high enough to leave room for an + * answer, and the **error body** was read rather than the status code. A + * reasoning model given a small budget returns 400 for an accepted value too + * ("max_tokens or model output limit was reached"), so a status-only probe + * reports a rejection that never happened. A real refusal says either + * "Validation failed" or, for gemini, "Reasoning is mandatory for this + * endpoint and cannot be disabled". + * + * Measured 2026-09-09. Re-probe before trusting these after a vendor change: + * an entry that has become wrong is worse than no entry. + */ +/** + * Models that accept any string in `reasoning_effort` — `banana` and `zzz9` both + * return 200 — so the parameter is not honoured and no ladder can be established + * by probing. The schema still lists one, inherited from the family template. + * + * Publishing it would state a control the caller does not have. Emitting nothing + * says only that we cannot describe it, which is the truth. Re-probe with a + * deliberately invalid value before adding a ladder back: a 400 means the field + * became real. + * + * Measured 2026-09-09 across all 87 entries that carry reasoning options; these + * were the only ones that failed the check. Re-probed 2026-09-10: five still + * accept `banana` with a 200 and stay. + * + * `moonshot/kimi-k3` was removed on 2026-09-10. Re-probed, its control answers + * 200 and a deliberately invalid effort answers 400 — the field is read and + * enforced, which is the one thing `[]` asserts is not true. Whether the levels + * ORDER is still unproven: `low` measured 919 and 1618 reasoning tokens and + * `high` measured 1943, so the spread within `low` is nearly the distance from + * `low` to `high`, and `max` timed out at the gateway on both attempts. The id + * therefore goes back to the schema-derived ladder its card declares, which is + * the host's own statement about itself, rather than to a claim of ours that + * the measurement does not support in either direction. + */ +/** + * Catalogue rows that cannot actually be called: `/v1/models` lists them with a + * name and prices, inference answers 404 "No endpoints found". Publishing one + * hands a reader a model id that fails on first use. + * + * Measured 2026-09-09; the sibling `-fast` and `-pro` aliases all answered 200, + * so this is one broken row rather than a broken family. Re-check before adding + * to this list — a row that starts working should come back. + * + * The guard sits on the publish path, so the entry is never created or updated. + * It does not delete a file that is already on disk: this sync runs with + * `deleteMissing: false`, so removing a published row still takes an explicit + * delete, as it did here. + */ +const NOT_CALLABLE: ReadonlySet = new Set([ + "anthropic/claude-opus-4.7-fast", + // Joined it on 2026-09-09: gone from `/v1/models` (785 rows, neither `-fast` + // id among them) and `/docs-json` answers 404 for it, so there is nothing + // left to describe. The host's own deprecation record calls both withdrawn. + "anthropic/claude-opus-4.8-fast", + // 2026-09-11: answers 404 to every request including the no-effort control — + // "0 endpoints out of 1 requested are available matching your guardrail + // restrictions and data policy", an OpenRouter refusal passed straight + // through. Found while probing its ladder; the ladder is moot. + "sakana/sakana-namazu", +]); + +/** + * Ladders the endpoint VALIDATES and then does not act on. + * + * Kept apart from `EFFORT_NOT_HONOURED` because the evidence is different and + * the two must not be confused. There, an invalid value returns 200, which + * proves the field is never read. Here an invalid value is rejected — the + * field is read, parsed and enforced — and the levels still fail to order. + * + * `moonshotai/kimi-k2-thinking`, measured 2026-09-10 on a prompt hard enough + * to make a difference visible: `low` produced 6723 and 8207 reasoning tokens + * across repeats, `high` produced 6508. Unset produced 2919 against `low`'s + * 2799 on a smaller budget. `low` above `high` is not a small delta in the + * wrong direction, it is the absence of an ordering, and the lab entry for + * this model publishes `reasoning_options = []` for the same reason: the + * model always reasons and the caller cannot steer it. + * + * Publishing a ladder here would sell a dial that turns nothing. + */ +/** + * ## Why this file reads `reasoning_effort` and nothing else + * + * The review asks repeatedly for `toggle` and `budget_tokens` options, on the + * grounds that the labs behind these models expose them and a relay should + * mirror the control family. The premise does not survive a control. + * + * Probed on 2026-09-10 against `deepseek-v4-pro`, `qwen-plus`, `kimi-k3` and + * `glm-5-turbo` on the OpenAI-compatible path, each alongside a no-field + * control that answered 200: + * + * enable_thinking: false 200 + * thinking: {type: 'disabled'} 200 + * thinking_budget: 128 200 + * banana_toggle: true 200 <- a field that exists nowhere + * + * An invented field is accepted exactly like the real ones, so this surface + * discards unknown top-level keys rather than forwarding them. A 200 on + * `enable_thinking` is therefore not evidence that a toggle exists; it is + * evidence that nothing is listening. Publishing `toggle` on that basis would + * hand callers a control the gateway throws away. + * + * `reasoning_effort` is different in kind: an invalid value is REJECTED on the + * models that read it, which is how this file tells a live control from a + * swallowed one, and it is why the two categories below are kept apart. + */ +const EFFORT_VALIDATED_BUT_INERT: ReadonlySet = new Set([ + "moonshotai/kimi-k2-thinking", + // 2026-09-11: an invalid value is rejected, and then `low` spends 2149 + // reasoning tokens to `high`'s 1533 with unset at 1801 between them. The + // lab publishes toggle only for this id, which the wire agrees with: there + // is thinking, and nothing the caller sends steers how much. + "z-ai/glm-4.7-flash", + // The other toggle-only labs, 2026-09-11, each on two repeats of low/high + // with the field validated. None orders, and the wire agrees with the lab: + // claude-haiku-4.5 188/182 vs 179/185 + // qwen3.6-max-preview 574/580 vs 552/554 (high below low, both times) + // gemma-4-31b-it reasoning count 1 at every rung + // mimo-v2.5-pro reasoning count 0 at every rung + // mimo-v2.5 307/234 vs 104/235 + // glm-4.5v 614/680 vs 641/643 + // glm-4.6v 191/116 vs 123/109 + // `claude-sonnet-4.5` is the one with a direction — 184/183 vs 220/210 — + // but a thirty-token delta on a model the host's own middleware lists as + // effort-unsupported (`strip-unsupported-effort.ts`) is not evidence + // enough to contradict a lab that publishes toggle only. Recorded so it can + // be revisited; it takes `[]` with the rest. + "anthropic/claude-haiku-4.5", + "anthropic/claude-sonnet-4.5", + "alibaba/qwen3.6-max-preview", + "google/gemma-4-31b-it", + "xiaomi/mimo-v2.5-pro", + "xiaomi/mimo-v2.5", + "z-ai/glm-4.5v", + "z-ai/glm-4.6v", +]); + +const EFFORT_NOT_HONOURED: ReadonlySet = new Set([ + // Re-verified 2026-09-10 by the token-scaling test the review asked for, + // rather than by status codes alone: `low` measured 3837 and 3841 reasoning + // tokens, `high` measured 3837 and 3838. Four runs, a spread of four tokens, + // no movement between the levels at all. Peers publish L/M/H for this id; + // this host accepts the names and steers nothing. + "google/gemini-3.1-pro-preview", + "google/gemma-4-26b-a4b-it", + "z-ai/glm-5v-turbo", + "z-ai/glm-5-turbo", + "alibaba/qwen-plus", +]); + +/** + * Ladders this host was measured to accept AND honour, where that differs from + * what its schema advertises. `accepted` is not enough on its own: a rung the + * endpoint takes and then ignores publishes worse advice than one it refuses, + * because the caller asks for more thinking, is told yes, and is billed for a + * reply produced with less. + */ +/** + * `deepseek-chat` and `deepseek-v4-flash` publish identical prices on purpose. + * + * The review read the coincidence as a unit-mapping bug and asked for a + * re-check. It is not one: DeepSeek retired `deepseek-chat` as a model and + * routes the id to its current Flash build, so the host bills it on the Flash + * grid — 0.3 in / 1.2 out / 0.006 cached, times the host margin, which is + * exactly the 0.39 / 1.56 / 0.0078 both rows carry. + * + * The check was worth running anyway. It turned up the real defect beside the + * prices: the row still carried V3's name and a 128K context for a model with + * 1M, which the host has since corrected. Kept here so the next reviewer does + * not spend the same round on it. + */ + +/** + * The OpenAI pro/codex/5.6 batch, probed 2026-09-10 because the review asked + * for each id to be intersected with a live accept-and-honour test. + * + * An invalid effort is rejected on all nine, so every ladder below is a live + * control rather than a swallowed field. + * + * The rungs the review wanted ADDED are refused here. `xhigh` answers 400 on + * `gpt-5.2-pro`, `gpt-5.5-pro` and `gpt-5.3-codex`; `max` answers 400 on + * `gpt-5.6-luna`. Publishing the lab's wider sets would hand callers an error, + * so the host's narrower enum stands. + * + * The rung the review worried was fake is real. `none` measured 0 reasoning + * tokens on every id that offers it — `gpt-5.3-codex`, `gpt-5.6-luna`, `-sol`, + * `-terra` and `-terra-pro` — against non-zero at `high` on each. That is the + * check Sonnet 5 failed, and these pass it. + * + * Ordering, on a deliberately light prompt so the batch could finish: + * + * gpt-5.2-codex low 90 high 192 ordered + * gpt-5.1-codex low 0 high 103 ordered + * gpt-5.6-terra low 9 high 25 ordered + * gpt-5.6-terra-pro low 91 high 129 ordered + * gpt-5.6-sol low 0 high 9 ordered, barely + * gpt-5.6-luna low 26 high 27 not separated + * gpt-5.5-pro low 33 high 31 not separated + * gpt-5.2-pro 0 at every rung — the model did not reason at all here + * + * The last three are left alone rather than trimmed. `gpt-5.2-pro` is the + * `kwaipilot` case from the Method section: when the control itself produces + * nothing, the cells say nothing about the ladder. The other two would need a + * harder prompt than this batch could afford, and a rung the host accepts is + * not removed on a probe that failed to separate anything. + */ +/** + * `none` on the Claude ladders, which the review asked to justify or drop. + * + * It is a real control, and what it does depends on which link serves the + * request — so the honest answer is per model rather than per family. + * + * `claude-opus-4.7` and `4.8` reach OpenRouter for every rung up to `high`, + * and only `xhigh`/`max` go to Anthropic natively. On that link `none` + * measured 0 reasoning tokens against 191 at `low` and 1228 at `high`: an off + * switch, and the reason `none` stays on those ids. + * + * `claude-sonnet-5` has no OpenRouter link, so every rung goes native, and + * Anthropic has no off switch on its adaptive models — `thinking: + * {type: 'disabled'}` returns 200 there and is ignored. The host used to drop + * the field entirely for `none`, which handed the request to Anthropic's + * default: 6000 output tokens spent thinking and an EMPTY reply. That is fixed + * upstream and `none` now maps to `low`, the cheapest rung the vendor has, so + * on this id it means minimum rather than off. + * + * Both are worth publishing — a caller sending `none` gets the least thinking + * available either way — but they are not the same promise, and this is where + * that is written down. + */ +/** + * `alibaba/qwen3.8-2.4t-a95b` keeps `high` as its top rung. + * + * The review asked for `xhigh` instead, on the peer ladder for this model. + * Probed twice: `xhigh` answers 400 with `Expected 'low' | 'medium' | 'high'`, + * so the host does not offer it and publishing it would hand callers an error. + * `high` at 4000 tokens times out at the gateway rather than returning, so + * whether the rungs order is still unmeasured — the enum, however, is settled. + */ +const MEASURED_EFFORTS: Readonly> = { + // schema offers none+minimal; both refused, `none` explicitly and by name. + // `max` is kept on evidence the review asked to see recorded here: measured + // 2131 reasoning tokens at `low`, 3841 at `high` and 6585 at `max` on one + // prompt, a monotone spread far outside the noise on this family. Peers stop + // at `high`; this host both accepts `max` and spends visibly more for it. + "google/gemini-3.7-flash": ["low", "medium", "high", "max"], + // schema offers none; refused as a validation error + "google/gemini-3.6-flash": ["minimal", "low", "medium", "high", "max"], + // The schema offers `xhigh` on both and the endpoint answers 200 for it, so + // no status-code probe could catch this. Pinned to the direct hop on a prompt + // hard enough to show the difference, `xhigh` produced 0 reasoning tokens on + // o1 and 192 on o3-mini, where `high` produced 10176 and 16000 and even `low` + // produced ~4500. The rung is accepted and dropped. The review asked for + // exactly this set on both ids and was right; the host has since stopped + // advertising `xhigh` here, so this entry becomes a no-op rather than an + // override once that reaches production. + // `low` measured indistinguishable from `medium` on a prompt hard enough to + // separate them: low 2230 and 2273 reasoning tokens, medium 2180 and 2420, + // overlapping ranges across repeats, while `high` sat clearly above at 2984. + // Two names for one behaviour is worse than one name, so `low` goes. The + // review asked for this too, on the different ground that the lab omits it; + // the measurement is what this entry rests on. `xhigh` is not added back — + // this host's schema stops at `high` and refuses it. + // Re-probed on a hard prompt at the review's suggestion, because the light + // one had `gpt-5.2-pro` not reasoning at all. `low` and `medium` come back + // indistinguishable on both — 1400 against 1311, and 2345 against 2336 — + // while `high` sits above where it could be measured at all. Same call as + // `gpt-5.4-pro` below: two names for one behaviour, so the lower goes. + "openai/gpt-5.2-pro": ["medium", "high"], + "openai/gpt-5.5-pro": ["medium", "high"], + "openai/gpt-5.4-pro": ["medium", "high"], + // `none` dropped 2026-09-10. On this id every rung reaches Anthropic + // natively, and Anthropic has no off switch on its adaptive models, so the + // host maps `none` to `low` — the cheapest real rung. Publishing it as + // `none` tells a caller reading the catalogue that reasoning can be turned + // off here, which is not what they would get. The sibling opus-4.7/4.8 keep + // `none` because their lower rungs reach OpenRouter, where it measured 0 + // reasoning tokens against 191 at `low`: there it really is off. + // `none` dropped 2026-09-10: it is accepted, but nothing shows it turns + // reasoning off. Two runs returned no `reasoning_tokens` field at all while + // still producing 1703 and 2000 output tokens, so there is no reading of + // that as an off switch — and `high` on the same model does report 2000 + // reasoning tokens. A rung whose semantics cannot be shown does not belong + // in a catalogue other people build against. + "alibaba/qwen3.8-max": ["low", "medium", "high"], + "anthropic/claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"], + // `medium` dropped 2026-09-10, and this is a correction to an earlier call + // in this file rather than a new measurement. It measured 5563 and then 7204 + // against `high`'s 5662: a spread inside the rung wider than any gap to the + // rung above. That was recorded as "unsettled, so keep it" — but publishing + // a rung is a claim too, and `gpt-5.4-pro` above had the identical shape and + // lost `low` for it. Two rules for one situation is worse than either rule. + // `none` (0 twice) and `low` (4175 against 5662) stay: those are measured. + "deepseek/deepseek-v4-pro": ["none", "low", "high"], + // Same cut on the dated snapshot, measured rather than copied: `medium` + // exhausted a 4000-token budget on reasoning twice while `high` used 2318. + // A rung that spends more than the one above it is not a lower rung. + "deepseek/deepseek-v4-pro-0813": ["none", "low", "high"], + "openai/o1": ["low", "medium", "high"], + "openai/o3-mini": ["low", "medium", "high"], +}; + +/** + * Models whose reasoning arrives on a side channel rather than in `content`. + * + * The review asked whether this gateway returns one, and it does — but the lab + * entries that declare it are NOT inherited through `base_model`, so the built + * catalogue showed `interleaved` unset on every aimlapi row while the provider + * was in fact emitting 20K characters of chain-of-thought in a separate field. + * A client reading only this provider would render that as the answer. + * + * Probed across all 81 ids that carry a reasoning ladder, 2026-09-10, by + * reading the keys actually present on the response message rather than by + * family: 14 return `reasoning_content`, 44 return `reasoning_details` + * alongside `reasoning`. + * + * Six ids the review asked about — `deepseek-chat`, the base `gpt-5.6-luna`, + * `-sol` and `-terra`, `o1` and `o3-mini` — return NO side channel, and that + * was re-checked under forced reasoning so it could not be the prompt: `o1` + * spent 2500 reasoning tokens and `gpt-5.6-luna` 1023 with nothing beside + * `content` on the message. The `-pro` siblings that do declare a field reach + * OpenRouter, which exposes one; the base ids reach OpenAI natively, which + * does not. Same family, different link, different wire. `deepseek-chat` is + * non-thinking by default and produced no reasoning tokens at all. + * + * Fifteen more return a bare `reasoning`, and two Gemini Flash ids return + * `extra_content`. Neither name is in the schema's enum, so those stay unset — + * recorded here so the gap reads as known rather than missed. Most of that + * group is the Anthropic line, where the field carries the summarised thinking + * block. + */ +const INTERLEAVED_FIELD: Readonly> = { + "alibaba/qwen3.5-flash": "reasoning_content", + "alibaba/qwen3.6-27b": "reasoning_content", + "alibaba/qwen3.6-35b-a3b": "reasoning_content", + "alibaba/qwen3.6-max-preview": "reasoning_content", + "alibaba/qwen3.7-max": "reasoning_content", + "alibaba/qwen3.8-flash": "reasoning_content", + "alibaba/qwen3.8-max": "reasoning_content", + "anthropic/claude-fable-5": "reasoning_content", + "anthropic/claude-sonnet-5": "reasoning_content", + "deepseek/deepseek-v4-flash": "reasoning_content", + "deepseek/deepseek-v4-flash-vision-exp": "reasoning_content", + "deepseek/deepseek-v4-pro": "reasoning_content", + "moonshot/kimi-k3": "reasoning_content", + "z-ai/glm-5.3-flash": "reasoning_content", + "alibaba/qwen3.8-2.4t-a95b": "reasoning_details", + "alibaba/qwen3.8-27b": "reasoning_details", + "arcee-ai/trinity-large-thinking": "reasoning_details", + "bytedance-seed/seed-2.0-code": "reasoning_details", + "bytedance-seed/seed-2.0-lite": "reasoning_details", + "bytedance-seed/seed-2.0-mini": "reasoning_details", + "deepseek/deepseek-v4-pro-0813": "reasoning_details", + "google/gemini-3.1-flash-lite-preview": "reasoning_details", + "google/gemini-3.1-pro-preview-customtools": "reasoning_details", + "google/gemini-flash-latest": "reasoning_details", + "inclusionai/ling-3.0-flash-fin": "reasoning_details", + "meta/muse-glimmer-30b": "reasoning_details", + "meta/muse-spark-1.1": "reasoning_details", + "meta/muse-spark-1.2": "reasoning_details", + "openai/gpt-5": "reasoning_details", + "openai/gpt-5-mini": "reasoning_details", + "openai/gpt-5-nano": "reasoning_details", + "openai/gpt-5-pro": "reasoning_details", + "openai/gpt-5.1-codex": "reasoning_details", + "openai/gpt-5.1-codex-max": "reasoning_details", + "openai/gpt-5.1-codex-mini": "reasoning_details", + "openai/gpt-5.2-codex": "reasoning_details", + "openai/gpt-5.2-pro": "reasoning_details", + "openai/gpt-5.3-codex": "reasoning_details", + "openai/gpt-5.4-pro": "reasoning_details", + "openai/gpt-5.5-pro": "reasoning_details", + "openai/gpt-5.6-luna-pro": "reasoning_details", + "openai/gpt-5.6-sol-pro": "reasoning_details", + "openai/gpt-5.6-terra-pro": "reasoning_details", + "openai/gpt-oss-120b": "reasoning_details", + "openai/gpt-oss-20b": "reasoning_details", + "openai/o1-pro": "reasoning_details", + "openai/o3-pro": "reasoning_details", + "poolside/laguna-s-2.1": "reasoning_details", + "poolside/laguna-xs-2.1": "reasoning_details", + "tencent/hy3": "reasoning_details", + "tencent/hy4-preview": "reasoning_details", + "thinkingmachines/inkling": "reasoning_details", + "thinkingmachines/inkling-small": "reasoning_details", + "xiaomi/mimo-v2.5": "reasoning_details", + "xiaomi/mimo-v2.5-pro": "reasoning_details", + "z-ai/glm-4.5v": "reasoning_details", + "z-ai/glm-4.6v": "reasoning_details", + "z-ai/glm-4.7-flash": "reasoning_details", +}; + +const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers"); + +const DOCS_CONCURRENCY = 8; + +const PricingUnit = z + .object({ + name: z.string().nullish(), + content: z.string().nullish(), + origin: z.string().nullish(), + price: z.number().nullish(), + per: z.number().nullish(), + }) + .passthrough(); + +const Info = z + .object({ + name: z.string().nullish(), + contextLength: z.number().int().nonnegative().nullish(), + outputMax: z.number().int().nonnegative().nullish(), + }) + .passthrough(); + +export const AimlapiModel = z + .object({ + id: z.string().min(1), + type: z.string().nullish(), + info: Info.nullish(), + modalities: z + .object({ + input: z.array(z.string()).nullish(), + output: z.array(z.string()).nullish(), + }) + .passthrough() + .nullish(), + pricing: z + .object({ + units: z.array(PricingUnit).nullish(), + }) + .passthrough() + .nullish(), + /** Attached by fetchModels; not part of the upstream payload. */ + reasoningEffort: z.array(z.string()).nullish(), + }) + .passthrough(); + +export const AimlapiResponse = z + .object({ + data: z.array(AimlapiModel).min(1), + }) + .passthrough(); + +export type AimlapiModel = z.infer; + +type Modality = "text" | "audio" | "image" | "video" | "pdf"; + +const MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]); + +function normalizeModalities(values: readonly string[] | null | undefined): Modality[] { + const seen = new Set(); + for (const value of values ?? []) { + const normalized = value.toLowerCase(); + if (MODALITIES.has(normalized)) seen.add(normalized as Modality); + } + if (seen.size === 0) seen.add("text"); + return [...seen]; +} + +/** + * Ids this host also serves on a non-text surface. + * + * The catalog lists an id once per endpoint type, and the chat-surface record of + * an image model claims text output. Measured 2026-09-04: + * `google/gemini-2.5-flash-image` appears both as `openai/image-generations` + * with `output: ["image"]` and as `openai/chat-completions` with + * `output: ["text"]`; the same holds for the `gemini-3-pro-image` and + * `gemini-3.1-flash-image` families. Judging a record only by its own modalities + * therefore admits image generators into a chat catalog. + * + * An id this host serves as a media model is not a text-only chat model, whatever + * its chat record claims. Populated from the whole response before any record is + * judged, because the answer is not in the record itself. + */ +const mediaOutputIDs = new Set(); + +function indexMediaOutputs(models: readonly AimlapiModel[]): void { + mediaOutputIDs.clear(); + for (const model of models) { + const declared = model.modalities?.output ?? []; + // `normalizeModalities` treats an empty list as text, so an undeclared + // record must not be read as evidence of anything. + if (declared.length === 0) continue; + if (normalizeModalities(declared).some((modality) => modality !== "text")) { + mediaOutputIDs.add(model.id); + } + } +} + +/** + * Ids that are a second spelling of a model already in the catalogue. + * + * The host lists a dotted id and a dashed one for the same Anthropic model — + * `anthropic/claude-opus-4.8` and `anthropic/claude-opus-4-8`, `claude-sonnet-4.6` + * and `claude-sonnet-4-6` — because the dashed form is an alias its router also + * answers to. `/v1/models` returns both as full rows, so a sync that trusts the + * listing publishes the same model twice under two ids. + * + * The rule is deliberately narrow, because "looks like an alias" is not enough + * to delete a row: the dotted twin must be present in the SAME payload and + * carry the SAME display name. Two genuinely different models that happen to + * collide on spelling would differ in one of those, and both survive. + * + * The dotted form wins because it is what the host's own docs and its + * `base_model` mapping use; the dashed alias keeps working for callers either + * way, it just stops being a catalogue entry of its own. + */ +const ALIAS_DUPLICATE_IDS = new Set(); + +/** `anthropic/claude-opus-4-8` -> `anthropic/claude-opus-4.8`; undefined if not that shape. */ +function dottedTwin(id: string): string | undefined { + const dotted = id.replace(/-(\d+)-(\d+)$/, "-$1.$2"); + return dotted === id ? undefined : dotted; +} + +function indexAliasDuplicates(models: readonly AimlapiModel[]): void { + ALIAS_DUPLICATE_IDS.clear(); + const nameByID = new Map(); + for (const model of models) nameByID.set(model.id, model.info?.name ?? undefined); + + for (const model of models) { + const twin = dottedTwin(model.id); + if (twin === undefined || !nameByID.has(twin)) continue; + if (nameByID.get(twin) !== nameByID.get(model.id)) continue; + ALIAS_DUPLICATE_IDS.add(model.id); + } +} + +function isChatTextModel(model: AimlapiModel): boolean { + if (model.type !== CHAT_COMPLETIONS_TYPE) return false; + // Cross-surface check first: the chat record of a media model does not admit + // to being one. + if (mediaOutputIDs.has(model.id)) return false; + const output = normalizeModalities(model.modalities?.output); + // A chat model whose output is not purely text is a media model riding the + // chat protocol, and does not belong in a chat catalog. + return output.length === 1 && output[0] === "text"; +} + +/** + * Lab entry this id is a host for. AI/ML API is an aggregator and authors none + * of these models, so every entry has to point at the lab file rather than + * restate it. + */ +/** + * Ids whose `base_model` is not the lab entry their own name resolves to. + * + * `deepseek-chat` is the case this exists for. Confirmed on production + * 2026-09-10 rather than inferred: three calls to that id each answered + * `"model": "deepseek-flash"`, the same string `deepseek-v4-flash` returns. + * DeepSeek retired the model + * behind that name and routes the id to its current Flash build, so pointing + * at the chat lab entry inherits the wrong everything: a 128K window for a + * model with 1M, and `reasoning = false` for one that reasons. The host itself + * bills the id on the Flash grid and has since corrected its own row's name + * and limits; this is the same correction on the catalogue side. + */ +/** + * Ids whose host ROW must not override the lab entry it factors onto. + * + * Only `deepseek/deepseek-chat`. The host has corrected its row since this + * began (it now reads "DeepSeek Chat (V4.1 Flash)", 1,048,576 context), but + * republishing either beside the Flash `base_model` would still contradict + * it: the lab has folded its Flash ids into V4.1 and lists the window as + * 1,000,000, and the host's 1,048,576 is a rounding of that same window, not a + * different one. Name and limits stay suppressed so the alias inherits the + * lab's figures exactly as `deepseek-v4-flash` does. + */ +const STALE_HOST_ROW: ReadonlySet = new Set([ + "deepseek/deepseek-chat", +]); + +/** + * Ids whose host display NAME is stale while the rest of the row is fine. + * + * `google/gemini-2.5-flash-lite` still reads "Gemini 2.5 Flash Lite Preview" + * on the host although the route serves the GA model the lab entry names + * "Gemini 2.5 Flash-Lite" — a label left over from its preview period, the + * same mistake corrected for `openai/o1`. The host-side fix is open + * (aimlapi/aimlapi#6178); until it ships the override is dropped so the lab's + * name inherits. Limits and everything else still come from the host row. + */ +const STALE_HOST_NAME: ReadonlySet = new Set([ + "google/gemini-2.5-flash-lite", +]); + +const BASE_MODEL_OVERRIDES: Readonly> = { + "deepseek/deepseek-chat": "deepseek/deepseek-v4-flash", +}; + +/** + * Ids that are aliases of another host id on the wire, keyed to the id whose + * measurements they share. `deepseek-chat` answers `"model": "deepseek-flash"` + * exactly as `deepseek-v4-flash` does, so every probe run against the latter + * — which rungs are honoured, whether `none` switches reasoning off, which + * field carries the chain of thought — holds for the alias, and the two cards + * must resolve identically rather than the alias falling through to the + * unmeasured default. Only the measurement tables read through this map; the + * host row itself (name, limits, allowlists) stays keyed by the alias's own id. + */ +const WIRE_ALIAS_OF: Readonly> = { + "deepseek/deepseek-chat": "deepseek/deepseek-v4-flash", +}; + +/** The id whose measurements apply to `id`: its wire alias target, else itself. */ +function measuredAs(id: string): string { + return WIRE_ALIAS_OF[id] ?? id; +} + +function baseModelFor(id: string): string | undefined { + return BASE_MODEL_OVERRIDES[id] ?? resolveModelMetadataBaseModel(id); +} + +/** + * The lab entry's own effort rungs, or `undefined` when it has none. + * + * This is the baseline the sync now derives from. Where the lab and this host + * disagree, the lab wins by construction — and that has a known price, paid + * on purpose after seven review rounds asked for it. Rungs this host accepts + * AND measurably honours are dropped because the lab does not list them: + * `low` on `deepseek-v4-pro` (4175 reasoning tokens against 5662 at `high`), + * `high` on `qwen3.8-max` (2000, spending its full budget), `max` on + * `gemini-3.7-flash` (6585 against 3841 at `high`, the largest effect measured + * anywhere in this file), and `low`/`medium` on `gpt-5-pro` (64/256/320 on a + * light prompt). Callers of those ids lose working levels. The measurements + * stay in the history so the decision can be reversed with evidence in hand. + * + * What the doctrine buys is that re-sync cannot reintroduce a rung the lab + * does not publish, and that this provider reads like its peers. + */ +/** What the lab's provider entry says about effort, three ways. */ +export type LabLadder = + | { kind: "effort"; values: readonly string[]; source: "lab" | "peer" } + | { kind: "no-effort"; source: "lab" | "peer" } // the entry exists and lists toggle/budget only + | { kind: "no-entry" }; // neither a first-party nor an OpenRouter entry for this base + +export function labEffortValues(baseModelID: string): readonly string[] | undefined { + const ladder = labLadder(baseModelID); + return ladder.kind === "effort" ? ladder.values : undefined; +} + +export function labLadder(baseModelID: string): LabLadder { + // The lab's ladder lives on its first-party PROVIDER entry, not on the + // shared model record: `models//.toml` carries name, family and + // capabilities and no `reasoning_options`. So this reads + // `providers//models/.toml`, the same file a reviewer means when + // they say "the lab publishes". + const slash = baseModelID.indexOf("/"); + if (slash === -1) return { kind: "no-entry" }; + // `base_model` names the METADATA lab; the ladder is under the PROVIDER + // directory, and for `zhipuai` -> `zai`, `meta` -> `llama` those differ. + // The first version of this lookup assumed they matched and silently kept + // the host enum for every GLM id — the review caught it from the TOMLs. + // The mapping is not one-to-one either: Meta's Llama cards live under + // `llama/` but its Muse cards under `meta/`, so both the mapped directory + // and the lab's own name are tried, and the lookup that assumed one + // directory per lab was the second silent miss of the same kind. + const metadataLab = baseModelID.slice(0, slash); + const labDirs = [providerDirForMetadataLab(metadataLab), metadataLab].filter( + (dir, index, all) => all.indexOf(dir) === index, + ); + const name = baseModelID.slice(slash + 1); + // A dated snapshot (`deepseek-v4-pro-0813`) has no lab entry of its own and + // is the same control surface as the model it snapshots, so it borrows that + // entry's ladder rather than falling through to the host's enum — which is + // what left `0813` wider than the sibling it is a copy of. + const candidates = [name, name.replace(/-\d{4}$/, "")].filter( + (candidate, index, all) => all.indexOf(candidate) === index, + ); + for (const labDir of labDirs) { + const own = readLadder(labDir, candidates, "lab"); + if (own.kind !== "no-entry") return own; + } + // No first-party entry. The next-best baseline is an established peer on + // the same surface: OpenRouter's card for the same lab id, which the review + // names as the reference whenever the lab is silent. Its ladder is used + // exactly like a lab ladder — intersected, never widened — and its + // toggle-only cards count as "no effort ladder" just as a lab's would. + // OpenRouter files Alibaba under `qwen/`, so every directory it uses for + // the lab is tried. Only when neither exists is the model unresolved. + const peerPaths = openrouterDirsForMetadataLab(metadataLab).flatMap((dir) => + candidates.map((candidate) => `${dir}/${candidate}`), + ); + return readLadder(PEER_DIR, peerPaths, "peer"); +} + +/** OpenRouter's provider directory: the peer baseline when the lab is silent. */ +const PEER_DIR = "openrouter"; + +function readLadder(providerDir: string, modelPaths: readonly string[], source: "lab" | "peer"): LabLadder { + let parsed: Record | undefined; + for (const candidate of modelPaths) { + const file = path.join(PROVIDERS_DIR, providerDir, "models", `${candidate}.toml`); + try { + parsed = Bun.TOML.parse(readFileSync(file, "utf8")) as Record; + break; + } catch { + /* try the next candidate */ + } + } + if (parsed === undefined) return { kind: "no-entry" }; + const opts = parsed["reasoning_options"]; + if (!Array.isArray(opts)) return { kind: "no-entry" }; + const effort = opts.find((o) => o && typeof o === "object" && (o as { type?: unknown }).type === "effort") as + | { values?: unknown } + | undefined; + return Array.isArray(effort?.values) + ? { kind: "effort", values: effort!.values as string[], source } + : { kind: "no-effort", source }; +} + +/** + * `gpt-5-pro` is worth a line here because it was argued over the longest. + * The lab publishes `["high"]`; this host accepts `low` and `medium` too and + * ordered them on a light prompt (64/256/320). The review asked for the same + * on a hard prompt before keeping the extras, and that probe cannot be run: + * every level times out at the gateway on it, three of three. Under the + * intersection the point is moot — the lab set wins and `["high"]` is what + * ships — but the light-prompt ordering is real and is recorded so nobody + * reads the narrower set as a finding that the rungs did nothing. + */ + +/** + * Toggle- or budget-only labs whose `reasoning_effort` on this host was + * nonetheless measured to be live, recorded so the decision not to publish + * them can be revisited with the evidence in hand: + * + * gemini-2.5-flash-lite low 863 -> high 2399 reasoning tokens + * qwen3.6-27b, qwen3.7-max, qwen3.6-35b-a3b + * `low`/`medium` select the lab's thinking budget: + * the gateway answers "must be greater than + * thinking_budget [8192]" / "[32768]" when + * max_tokens is below them. + * + * They are NOT published. The baseline rule is lab ∩ host, and a lab that + * lists only a toggle or a budget has no effort ladder to intersect with; an + * effort list here would be the host's control family under a name the lab + * and its peers (OpenRouter: toggle only) do not use. The review held that + * line across several rounds and it is the line this file follows: these + * four reasoners are unresolved and skipped until either the lab publishes + * an effort ladder or this host exposes the budget itself. The measurements + * above stay so nobody reads the skip as a finding that the field did nothing. + */ +export const HOST_EFFORT_MEASURED_BUT_UNPUBLISHED: ReadonlySet = new Set([ + "google/gemini-2.5-flash-lite", + "alibaba/qwen3.6-27b", + "alibaba/qwen3.7-max", + "alibaba/qwen3.6-35b-a3b", +]); + +/** + * Ids where `reasoning_effort: none` is a measured off switch on this wire — + * zero reasoning tokens against a non-zero count at a higher rung, with an + * invalid value rejected so the field is known to be read. The lab sets never + * carry `none`, so the intersection strips it; this is the only way back in. + * + * NOT here, deliberately: `claude-sonnet-5`, where `none` maps to `low`, and + * `qwen3.8-max`, where it never reported a reasoning count at all. + */ +const NONE_IS_REAL_OFF: ReadonlySet = new Set([ + "deepseek/deepseek-v4-pro", + "deepseek/deepseek-v4-pro-0813", + // Flash measured by the strongest signal there is, the side channel itself: + // `none` returns an EMPTY `reasoning_content` where `high` returns 23469 + // characters on `v4-flash` and 12826 on `vision-exp`. `reasoning_tokens` is + // absent rather than 0 on this link, which is why the count alone was not + // enough to call it — the same shape was left unclaimed on `qwen3.8-max`. + "deepseek/deepseek-v4-flash", + "deepseek/deepseek-v4-flash-vision-exp", + "openai/gpt-5.3-codex", + "openai/gpt-5.6-luna", + "openai/gpt-5.6-sol", + "openai/gpt-5.6-terra", + "openai/gpt-5.6-terra-pro", + "anthropic/claude-opus-4.7", + "anthropic/claude-opus-4.8", +]); + +function baseReasoning(baseModelID: string): boolean { + try { + return modelMetadata(baseModelID).reasoning === true; + } catch { + return false; + } +} + +/** + * Prices are quoted as `price` per `per` tokens; models.dev stores dollars per + * million. The unit discriminator is `origin`, not `measure`: provided is + * input, generated is output, cached is a cache read. Only text token charges + * are taken — a model's image or audio units are a different surface. + */ +function perMillion(units: readonly z.infer[], origin: string): number | undefined { + const unit = units.find( + (candidate) => candidate.name === "token" && candidate.content === "text" && candidate.origin === origin, + ); + if (!unit || unit.price == null || !unit.per) return undefined; + // Round before returning. `(0.0000000078 / 1) * 1e6` is + // 0.0078000000000000005 in IEEE double, and `formatNumber` serializes the + // residue verbatim — so the catalogue carried a price with fifteen decimals + // that changes shape whenever the upstream `per` does. Six decimals is finer + // than any published USD/MTok figure here. + return Math.round(((unit.price / unit.per) * 1_000_000) * 1e6) / 1e6; +} + +function positive(value: number | null | undefined): number | undefined { + return value != null && value > 0 ? value : undefined; +} + +/** + * Reads the documented `reasoning_effort` values for one model. Returns + * undefined when the docs do not describe the control, which is treated as + * "cannot state it" rather than "the model has none". + * + * One model's schema can carry the control more than once, because the request + * body is a union of per-family variants and several of them accept it with + * different ladders. Taking the first one found means taking whichever variant + * happens to be earliest in the document, which drops real values: at the time + * of writing that costs `minimal` on the gpt-5 family, `max` on claude-sonnet-4.6 + * and claude-opus-4.8, and `none` on the gemini flash models. The union across + * every occurrence is what the endpoint actually accepts. + */ +async function fetchReasoningEffort(id: string, base: string): Promise { + const url = `${DOCS_ENDPOINT}?model=${encodeURIComponent(id)}&endpoint=${encodeURIComponent(CHAT_COMPLETIONS_TYPE)}`; + let payload: unknown; + try { + const response = await fetch(url); + if (!response.ok) return undefined; + payload = await response.json(); + } catch { + return undefined; + } + + const measuredId = measuredAs(id); + if (EFFORT_NOT_HONOURED.has(measuredId) || EFFORT_VALIDATED_BUT_INERT.has(measuredId)) return undefined; + + const found = new Set(); + collectReasoningEffortEnums(payload, found); + + const host = [...found] + .filter((value) => EFFORT_RANK.has(value)) + .sort((a, b) => EFFORT_RANK.get(a)! - EFFORT_RANK.get(b)!); + if (host.length === 0) return undefined; + + return resolveLadder(id, base, host); +} + +/** + * The published ladder for `id`, given the rungs its host schema accepts. + * Pure, so the rules can be tested without the network: + * + * 1. Intersect with the lab's own ladder where it has one. A rung the host + * offers but the lab does not is dropped even when it measures as working + * — that is the doctrine, costed on `labEffortValues`. An empty + * intersection keeps the host enum: it means the lab's rungs are refused + * here, not that no control exists. + * 2. `none` is ours, not the lab's: kept only where the lab lists it or a + * probe showed a real off switch (`NONE_IS_REAL_OFF`), lab ladder or not. + * 3. A measurement may narrow the result — an accepted rung that turns out + * inert — but never widen it past the lab. + */ +export function resolveLadder(id: string, base: string, host: readonly string[]): string[] | undefined { + const ladder = labLadder(base); + let values: string[]; + let lab: readonly string[] | undefined; + // A peer's `none` is that relay's own off mapping, not the lab's word that + // the rung exists; it is not taken on trust the way a lab-listed `none` is. + const labSpeaks = ladder.kind !== "no-entry" && ladder.source === "lab"; + + switch (ladder.kind) { + case "effort": { + lab = ladder.values; + const shared = host.filter((value) => lab!.includes(value)); + // Every lab rung refused here. That is a contradiction, not a licence + // to publish the host's rungs under the lab's name: unresolved, so the + // model is skipped rather than shipped with an invented ladder. + if (shared.length === 0) return undefined; + values = shared; + break; + } + case "no-effort": + // The lab (or, failing it, the peer) has spoken and said "toggle / + // budget, no effort ladder". There is nothing to intersect with, and a + // host effort enum would claim a control family the baseline denies — + // even where the field measured live (see + // HOST_EFFORT_MEASURED_BUT_UNPUBLISHED). `[]` would claim no control, + // which a validated field rules out. So the model is unresolved and + // skipped. + return undefined; + case "no-entry": + // Neither the lab nor the peer publishes a ladder for this model, so + // there is nothing to intersect with and the host's enum would ship as + // a bare schema dump. Unresolved: the model is skipped until a baseline + // exists rather than given boilerplate rungs. + return undefined; + } + + const measuredId = measuredAs(id); + const labListsNone = labSpeaks && lab !== undefined && lab.includes("none"); + const noneAllowed = labListsNone || NONE_IS_REAL_OFF.has(measuredId); + if (!noneAllowed) values = values.filter((value) => value !== "none"); + else if (host.includes("none") && !values.includes("none")) values = ["none", ...values]; + + const measured = MEASURED_EFFORTS[measuredId]; + if (measured) values = values.filter((value) => measured.includes(value)); + + return values.length > 0 ? values : undefined; +} + +function collectReasoningEffortEnums(node: unknown, into: Set): void { + if (Array.isArray(node)) { + for (const item of node) collectReasoningEffortEnums(item, into); + return; + } + if (node === null || typeof node !== "object") return; + + const record = node as Record; + const effort = record["reasoning_effort"]; + if (effort !== null && typeof effort === "object") { + const values = (effort as Record)["enum"]; + if (Array.isArray(values) && values.every((value) => typeof value === "string")) { + for (const value of values as string[]) into.add(value); + } + } + + // Keep walking either way: the same schema can describe the control again in + // a sibling variant of the request-body union. + for (const value of Object.values(record)) collectReasoningEffortEnums(value, into); +} + +async function attachReasoningEffort(models: AimlapiModel[]): Promise { + // Only models whose lab entry says they reason need the control documented, + // and only those are worth a request. + const pending = models.filter((model) => { + if (NOT_CALLABLE.has(model.id)) return false; + if (ALIAS_DUPLICATE_IDS.has(model.id)) return false; + if (!isChatTextModel(model)) return false; + const base = baseModelFor(model.id); + return base !== undefined && baseReasoning(base); + }); + + let cursor = 0; + const workers = Array.from({ length: Math.min(DOCS_CONCURRENCY, pending.length) }, async () => { + while (cursor < pending.length) { + const model = pending[cursor++]; + if (model === undefined) return; + const base = baseModelFor(model.id); + if (base === undefined) continue; + model.reasoningEffort = await fetchReasoningEffort(model.id, base); + } + }); + await Promise.all(workers); +} + +/** + * The leading comment a card carries so a reader can see what the wire does + * without opening this file. Only cards whose control story is not plain + * "lab ladder ∩ host enum" get one — the ones a review has to ask about. + */ +function wireHeader(id: string, reasoningOptions: Array<{ type: "effort"; values: EffortValue[] }> | undefined): string | undefined { + const measuredId = measuredAs(id); + const lines: string[] = []; + if (measuredId !== id) { + lines.push(`# Alias: answers "model" as ${measuredId.slice(measuredId.indexOf("/") + 1)}; measured through that id.`); + } + if (reasoningOptions !== undefined && reasoningOptions.length === 0) { + lines.push( + EFFORT_NOT_HONOURED.has(measuredId) + ? "# No caller control: reasoning_effort is not read (an invalid value returns 200)," + : "# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high", + EFFORT_NOT_HONOURED.has(measuredId) + ? "# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200)." + : "# do not order reasoning tokens; toggle/budget fields are dropped by the gateway.", + ); + } + if (NONE_IS_REAL_OFF.has(measuredId)) { + lines.push("# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface."); + } + return lines.length > 0 ? lines.join("\n") + "\n" : undefined; +} + +export const aimlapi = { + id: "aimlapi", + name: "AI/ML API", + modelsDir: "providers/aimlapi/models", + // The catalog turns over quickly and lists far more than the chat surface, so + // a local model missing from one response is not proof that it is gone. + deleteMissing: false, + authoritativeHeaders: true, + sourceID(model) { + if (NOT_CALLABLE.has(model.id)) return undefined; + if (ALIAS_DUPLICATE_IDS.has(model.id)) return undefined; + return isChatTextModel(model) ? model.id : undefined; + }, + skippedNotice(ids) { + if (ids.length === 0) return []; + return [ + `${ids.length} AI/ML API chat models were skipped because this repository has no lab entry to point \`base_model\` at, or because the API does not document the reasoning control a reasoning model requires.`, + `Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`, + ]; + }, + missingNotice(paths) { + if (paths.length === 0) return []; + return [ + `${paths.length} local AI/ML API models were absent from the catalog and were retained for manual lifecycle review.`, + `Retained local paths: ${paths.map((item) => `\`${item}\``).join(", ")}`, + ]; + }, + async fetchModels() { + const response = await fetch(API_ENDPOINT); + if (!response.ok) { + throw new Error(`AI/ML API request failed: ${response.status} ${response.statusText}`); + } + const raw = await response.json(); + const parsed = AimlapiResponse.parse(raw); + indexMediaOutputs(parsed.data); + indexAliasDuplicates(parsed.data); + await attachReasoningEffort(parsed.data); + return parsed; + }, + parseModels(raw) { + const models = AimlapiResponse.parse(raw).data; + // Replays parse a cached payload without going through fetchModels. + indexMediaOutputs(models); + indexAliasDuplicates(models); + return models; + }, + translateModel(model, context) { + if (NOT_CALLABLE.has(model.id)) return undefined; + if (ALIAS_DUPLICATE_IDS.has(model.id)) return undefined; + if (!isChatTextModel(model)) return undefined; + + const existing = context.existing(model.id); + + // AI/ML API hosts other people's models, so the entry must reference the + // lab file instead of duplicating it. Without a lab entry to point at there + // is nothing correct to write: inlining the metadata is what this schema + // forbids, and authoring the lab file would mean sourcing capability data + // the catalog does not publish. + const base = existing?.base_model ?? baseModelFor(model.id); + if (base === undefined) return undefined; + + // Required whenever the base model reasons. Only the API's own request + // schema can say which values it takes, so a model whose docs stay silent + // is skipped rather than given an invented control. + let reasoningOptions: Array<{ type: "effort"; values: EffortValue[] }> | undefined; + if (baseReasoning(base)) { + // A model that reasons but honours no caller control gets an empty list: + // the schema requires the field, and an empty one states the truth — + // reasoning happens, nothing about it is selectable. + if (EFFORT_NOT_HONOURED.has(measuredAs(model.id)) || EFFORT_VALIDATED_BUT_INERT.has(measuredAs(model.id))) { + reasoningOptions = []; + } else { + const values = model.reasoningEffort?.filter(isEffortValue); + if (values === undefined || values.length === 0) return undefined; + reasoningOptions = [{ type: "effort", values }]; + } + } + + const units = model.pricing?.units ?? []; + const info = model.info ?? {}; + const contextLimit = positive(info.contextLength); + const outputLimit = positive(info.outputMax); + // Only what the catalog actually publishes. It reports a context window and + // an output cap but no input cap, and equating the input cap with the whole + // context would overwrite the lab's correct split (e.g. 272k in + 128k out + // within a 400k window) with a wrong number. + const limit = + STALE_HOST_ROW.has(model.id) || + (contextLimit === undefined && outputLimit === undefined) + ? undefined + : { + ...(contextLimit === undefined ? {} : { context: contextLimit }), + ...(outputLimit === undefined ? {} : { output: outputLimit }), + }; + + // Everything else — the capability flags, description, dates, modalities — + // is the lab's to state and is inherited. factorBaseModel drops whatever + // matches the base, so the file carries only what is genuinely ours. + // A row the host lists but does not price is not sellable through it, and + // the schema (rightly) will not carry a card without a price. Skip it + // rather than invent zeros. + const inputCost = perMillion(units, "provided") ?? existing?.cost?.input; + const outputCost = perMillion(units, "generated") ?? existing?.cost?.output; + if (inputCost === undefined || outputCost === undefined) return undefined; + + return { + id: model.id, + header: wireHeader(model.id, reasoningOptions), + model: factorBaseModel( + base, + { + cost: { + input: inputCost, + output: outputCost, + cache_read: perMillion(units, "cached") ?? existing?.cost?.cache_read, + }, + // Aliases such as `-pro` and `-fast` factor onto the base model and + // would inherit its display name, so the catalogue would list two rows + // called "GPT-5.6 Luna". The host names them apart; carry that through + // and the override drops itself when the names already agree. + name: + STALE_HOST_ROW.has(model.id) || STALE_HOST_NAME.has(model.id) ? undefined : (info.name ?? undefined), + reasoning_options: reasoningOptions, + interleaved: INTERLEAVED_FIELD[measuredAs(model.id)] + ? { field: INTERLEAVED_FIELD[measuredAs(model.id)]! } + : undefined, + limit, + }, + limit, + existing?.base_model === base ? existing?.base_model_omit : undefined, + ), + }; + }, +} satisfies SyncProvider; diff --git a/packages/core/src/sync/providers/openrouter.ts b/packages/core/src/sync/providers/openrouter.ts index 77164f7caa6..1aca1935ef0 100644 --- a/packages/core/src/sync/providers/openrouter.ts +++ b/packages/core/src/sync/providers/openrouter.ts @@ -51,6 +51,36 @@ const CANONICAL_PROVIDER_PREFIXES = { "zai-org": { provider: "zai", metadata: "zhipuai" }, } as const; +/** + * The first-party provider directory for a metadata lab id. + * + * `base_model` ids are spelled with the METADATA lab (`zhipuai/glm-4.7-flash`, + * `meta/llama-...`) while a lab's own reasoning ladder lives under its + * PROVIDER directory (`providers/zai`, `providers/llama`). The two coincide for + * most labs and not for these, and a lookup that assumes they coincide fails + * silently — the caller falls back to whatever it had, which for a relay means + * publishing the host's enum as if it were the lab's. + */ +export function providerDirForMetadataLab(metadata: string): string { + for (const entry of Object.values(CANONICAL_PROVIDER_PREFIXES)) { + if (entry.metadata === metadata) return entry.provider; + } + return metadata; +} + +/** + * The OpenRouter model-directory prefixes that carry a metadata lab, in table + * order — e.g. `alibaba` lives under both `alibaba/` and `qwen/` on OpenRouter, + * and only the latter is populated. + */ +export function openrouterDirsForMetadataLab(metadata: string): string[] { + const dirs: string[] = []; + for (const [prefix, entry] of Object.entries(CANONICAL_PROVIDER_PREFIXES)) { + if (entry.metadata === metadata) dirs.push(prefix); + } + return dirs.length > 0 ? dirs : [metadata]; +} + export const OpenRouterModel = z.object({ id: z.string(), name: z.string(), diff --git a/packages/core/test/aimlapi.test.ts b/packages/core/test/aimlapi.test.ts new file mode 100644 index 00000000000..fa2d19242fd --- /dev/null +++ b/packages/core/test/aimlapi.test.ts @@ -0,0 +1,121 @@ +import { expect, test } from "bun:test"; + +import { labEffortValues, labLadder, resolveLadder } from "../src/sync/providers/aimlapi.js"; +import { providerDirForMetadataLab } from "../src/sync/providers/openrouter.js"; + +// The lookup that silently kept the host enum for every GLM id: `base_model` +// names the metadata lab, the ladder lives under the provider directory, and +// for these two they differ. +test("maps a metadata lab to its first-party provider directory", () => { + expect(providerDirForMetadataLab("zhipuai")).toBe("zai"); + expect(providerDirForMetadataLab("deepseek")).toBe("deepseek"); + expect(providerDirForMetadataLab("no-such-lab")).toBe("no-such-lab"); +}); + +test("a lab split across two provider directories is read from both", () => { + // Meta's Llama cards live under `llama/`, its Muse cards under `meta/`. + expect(providerDirForMetadataLab("meta")).toBe("llama"); + expect(labLadder("meta/muse-spark-1.1")).toEqual({ + kind: "effort", + values: ["minimal", "low", "medium", "high", "xhigh"], + source: "lab", + }); + expect(resolveLadder("meta/muse-spark-1.1", "meta/muse-spark-1.1", ["low", "medium", "high"])).toEqual([ + "low", + "medium", + "high", + ]); +}); + +test("reads the lab ladder through the provider directory, not the model record", () => { + // `models/zhipuai/*.toml` has no reasoning_options; `providers/zai/models` + // does. A lookup on the wrong one returns undefined and looks like "no lab + // ladder", which is a different claim. + expect(labEffortValues("zhipuai/glm-5.3-flash")).toEqual(["low", "high", "max"]); + expect(labEffortValues("deepseek/deepseek-v4-pro")).toEqual(["high", "max"]); +}); + +test("a dated snapshot borrows the ladder of the model it snapshots", () => { + // There is no `deepseek-v4-pro-0813.toml` in the lab; without the borrow it + // fell through to the host enum and came out wider than its own base. + expect(labEffortValues("deepseek/deepseek-v4-pro-0813")).toEqual(labEffortValues("deepseek/deepseek-v4-pro")); +}); + +test("a lab entry with toggle only and no effort ladder yields undefined", () => { + expect(labEffortValues("zhipuai/glm-4.7-flash")).toBeUndefined(); +}); + +const HOST = ["none", "low", "medium", "high"] as const; + +test("intersects the host enum with the lab ladder and drops the rest", () => { + // lab high/max, host none..high: `low` and `medium` go although the host + // accepts them — that is the doctrine, and this test pins its cost. + expect(resolveLadder("deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-pro", HOST)).toEqual(["none", "high"]); +}); + +test("`none` survives only where measured as a real off switch", () => { + // Same lab, same host enum; only the measurement differs. + expect(resolveLadder("deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-pro", HOST)).toContain("none"); + expect(resolveLadder("deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash", HOST)).toContain("none"); +}); + +test("`none` is kept where the lab itself lists it", () => { + const host = ["none", "low", "medium", "high"]; + expect(resolveLadder("openai/gpt-5.6-luna", "openai/gpt-5.6-luna", host)).toContain("none"); +}); + +test("a measurement narrows but never widens past the lab", () => { + // gpt-5.4-pro: lab medium/high/xhigh, host low/medium/high, measured medium/high. + expect(resolveLadder("openai/gpt-5.4-pro", "openai/gpt-5.4-pro", ["low", "medium", "high"])).toEqual(["medium", "high"]); +}); + +// The fallthrough the review found: a toggle-only lab must never turn into a +// host-schema L/M/H dump. Either the host field is measured live for that id, +// or the model is unresolved. +test("a toggle-only lab base never yields the host's effort dump", () => { + const host = ["none", "minimal", "low", "medium", "high"]; + // glm-4.5v: lab toggle-only -> unresolved. + expect(resolveLadder("z-ai/glm-4.5v", "zhipuai/glm-4.5v", host)).toBeUndefined(); + // qwen3.7-max: lab toggle+budget. The host's effort field measured live + // (its names select the budget tiers), and it is still unresolved: a + // toggle/budget baseline has no effort ladder to intersect with. + expect(resolveLadder("alibaba/qwen3.7-max", "alibaba/qwen3.7-max", host)).toBeUndefined(); + expect(resolveLadder("google/gemini-2.5-flash-lite", "google/gemini-2.5-flash-lite", host)).toBeUndefined(); +}); + +test("an empty intersection is unresolved, not the host enum", () => { + // lab high/max, host offers neither -> skip, do not republish low/medium. + expect(resolveLadder("deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-pro", ["low", "medium"])).toBeUndefined(); +}); + +test("a base with no lab entry falls back to the OpenRouter peer's ladder, intersected", () => { + // No `providers/tencent/models/hy3.toml`; OpenRouter lists none/low/high. + expect(labLadder("tencent/hy3")).toEqual({ kind: "effort", values: ["none", "low", "high"], source: "peer" }); + expect(resolveLadder("tencent/hy3", "tencent/hy3", ["low", "medium", "high"])).toEqual(["low", "high"]); + // A peer-listed `none` is not taken on trust the way a lab-listed one is. + expect(resolveLadder("tencent/hy3", "tencent/hy3", ["none", "low", "medium", "high"])).toEqual(["low", "high"]); + // OpenRouter files Alibaba under `qwen/`. + expect(labLadder("alibaba/qwen3.8-27b")).toEqual({ kind: "effort", values: ["low", "medium", "xhigh"], source: "peer" }); +}); + +test("a peer that is toggle-only counts as no effort ladder, not as a licence for the host enum", () => { + // No lab entry for Opus 4.1; OpenRouter's card is toggle-only. + expect(labLadder("anthropic/claude-opus-4.1")).toEqual({ kind: "no-effort", source: "peer" }); + expect(resolveLadder("anthropic/claude-opus-4.1", "anthropic/claude-opus-4.1", ["low", "medium", "high"])).toBeUndefined(); +}); + +test("a base with neither a lab nor a peer entry is unresolved, not the host enum", () => { + expect(labLadder("nolab/nothing-here")).toEqual({ kind: "no-entry" }); + expect(resolveLadder("some/model", "nolab/nothing-here", ["low", "high"])).toBeUndefined(); +}); + +test("a wire alias resolves through the measurements of the id it answers as", () => { + const host = ["none", "low", "medium", "high"]; + // `deepseek-chat` answers `"model": "deepseek-flash"` exactly as + // `deepseek-v4-flash` does, so the probes run against the latter (`none` + // switches reasoning off; `medium` is inert) apply to the alias too. + expect(resolveLadder("deepseek/deepseek-chat", "deepseek/deepseek-v4-flash", host)).toEqual( + resolveLadder("deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-flash", host), + ); + expect(resolveLadder("deepseek/deepseek-chat", "deepseek/deepseek-v4-flash", host)).toEqual(["none", "low", "high"]); +}); diff --git a/providers/aimlapi/logo.svg b/providers/aimlapi/logo.svg new file mode 100755 index 00000000000..5bd69eb5625 --- /dev/null +++ b/providers/aimlapi/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/providers/aimlapi/models/alibaba/qwen-max.toml b/providers/aimlapi/models/alibaba/qwen-max.toml new file mode 100644 index 00000000000..b67b2dfda47 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen-max.toml @@ -0,0 +1,9 @@ +base_model = "alibaba/qwen-max" + +[cost] +input = 2.08 +output = 8.32 +cache_read = 0.416 + +[limit] +context = 32_000 diff --git a/providers/aimlapi/models/alibaba/qwen-plus.toml b/providers/aimlapi/models/alibaba/qwen-plus.toml new file mode 100644 index 00000000000..c031d384512 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen-plus.toml @@ -0,0 +1,13 @@ +# No caller control: reasoning_effort is not read (an invalid value returns 200), +# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200). +base_model = "alibaba/qwen-plus" +reasoning_options = [] + +[cost] +input = 0.52 +output = 1.56 +cache_read = 0.104 + +[limit] +context = 131_000 +output = 16_384 diff --git a/providers/aimlapi/models/alibaba/qwen3-coder-480b-a35b-instruct.toml b/providers/aimlapi/models/alibaba/qwen3-coder-480b-a35b-instruct.toml new file mode 100644 index 00000000000..f1ee9a33c56 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3-coder-480b-a35b-instruct.toml @@ -0,0 +1,9 @@ +base_model = "alibaba/qwen3-coder-480b-a35b-instruct" +name = "Qwen3 Coder" + +[cost] +input = 1.95 +output = 9.75 + +[limit] +context = 262_000 diff --git a/providers/aimlapi/models/alibaba/qwen3-max.toml b/providers/aimlapi/models/alibaba/qwen3-max.toml new file mode 100644 index 00000000000..a4de21068d7 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3-max.toml @@ -0,0 +1,7 @@ +base_model = "alibaba/qwen3-max" +name = "Qwen3 Max Instruct" + +[cost] +input = 1.56 +output = 7.8 +cache_read = 0.312 diff --git a/providers/aimlapi/models/alibaba/qwen3-next-80b-a3b-instruct.toml b/providers/aimlapi/models/alibaba/qwen3-next-80b-a3b-instruct.toml new file mode 100644 index 00000000000..4f84b4071c5 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3-next-80b-a3b-instruct.toml @@ -0,0 +1,10 @@ +base_model = "alibaba/qwen3-next-80b-a3b-instruct" +name = "Qwen3 Next 80B A3B Instruct" + +[cost] +input = 0.195 +output = 1.56 + +[limit] +context = 129_024 +output = 16_384 diff --git a/providers/aimlapi/models/alibaba/qwen3.6-max-preview.toml b/providers/aimlapi/models/alibaba/qwen3.6-max-preview.toml new file mode 100644 index 00000000000..58be84db58e --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3.6-max-preview.toml @@ -0,0 +1,11 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "alibaba/qwen3.6-max-preview" +reasoning_options = [] + +[interleaved] +field = "reasoning_content" + +[cost] +input = 0.65 +output = 3.9 diff --git a/providers/aimlapi/models/alibaba/qwen3.8-2.4t-a95b.toml b/providers/aimlapi/models/alibaba/qwen3.8-2.4t-a95b.toml new file mode 100644 index 00000000000..642df5ea6fb --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3.8-2.4t-a95b.toml @@ -0,0 +1,16 @@ +base_model = "alibaba/qwen3.8-2.4t-a95b" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium"] + +[cost] +input = 3.4385 +output = 8.59625 +cache_read = 0.6877 + +[limit] +output = 52_429 diff --git a/providers/aimlapi/models/alibaba/qwen3.8-27b.toml b/providers/aimlapi/models/alibaba/qwen3.8-27b.toml new file mode 100644 index 00000000000..0d757197e19 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3.8-27b.toml @@ -0,0 +1,15 @@ +base_model = "alibaba/qwen3.8-27b" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium"] + +[cost] +input = 0.61893 +output = 4.40128 + +[limit] +output = 131_072 diff --git a/providers/aimlapi/models/alibaba/qwen3.8-flash.toml b/providers/aimlapi/models/alibaba/qwen3.8-flash.toml new file mode 100644 index 00000000000..72dd3bc6871 --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3.8-flash.toml @@ -0,0 +1,13 @@ +base_model = "alibaba/qwen3.8-flash" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium"] + +[cost] +input = 0.208 +output = 0.611 +cache_read = 0.0208 diff --git a/providers/aimlapi/models/alibaba/qwen3.8-max.toml b/providers/aimlapi/models/alibaba/qwen3.8-max.toml new file mode 100644 index 00000000000..56fbe364cfa --- /dev/null +++ b/providers/aimlapi/models/alibaba/qwen3.8-max.toml @@ -0,0 +1,13 @@ +base_model = "alibaba/qwen3.8-max" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium"] + +[cost] +input = 2.6 +output = 7.8 +cache_read = 0.325 diff --git a/providers/aimlapi/models/anthropic/claude-fable-5.toml b/providers/aimlapi/models/anthropic/claude-fable-5.toml new file mode 100644 index 00000000000..c5ea100ab0e --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-fable-5.toml @@ -0,0 +1,13 @@ +base_model = "anthropic/claude-fable-5" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 13 +output = 65 +cache_read = 1.3 diff --git a/providers/aimlapi/models/anthropic/claude-haiku-4.5.toml b/providers/aimlapi/models/anthropic/claude-haiku-4.5.toml new file mode 100644 index 00000000000..2381e2a5861 --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-haiku-4.5.toml @@ -0,0 +1,10 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "anthropic/claude-haiku-4-5" +name = "Claude 4.5 Haiku" +reasoning_options = [] + +[cost] +input = 1.3754 +output = 6.877 +cache_read = 0.13754 diff --git a/providers/aimlapi/models/anthropic/claude-opus-4.5.toml b/providers/aimlapi/models/anthropic/claude-opus-4.5.toml new file mode 100644 index 00000000000..2019df716b4 --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-opus-4.5.toml @@ -0,0 +1,11 @@ +base_model = "anthropic/claude-opus-4-5" +name = "Claude 4.5 Opus" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 6.877 +output = 34.385 +cache_read = 0.6877 diff --git a/providers/aimlapi/models/anthropic/claude-opus-4.7.toml b/providers/aimlapi/models/anthropic/claude-opus-4.7.toml new file mode 100644 index 00000000000..a2780ee040c --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-opus-4.7.toml @@ -0,0 +1,12 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "anthropic/claude-opus-4-7" +name = "Claude 4.7 Opus" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh", "max"] + +[cost] +input = 6.877 +output = 34.385 +cache_read = 0.6877 diff --git a/providers/aimlapi/models/anthropic/claude-opus-4.8.toml b/providers/aimlapi/models/anthropic/claude-opus-4.8.toml new file mode 100644 index 00000000000..16aac2d98aa --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-opus-4.8.toml @@ -0,0 +1,12 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "anthropic/claude-opus-4-8" +name = "Claude 4.8 Opus" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high", "xhigh", "max"] + +[cost] +input = 6.877 +output = 34.385 +cache_read = 0.6877 diff --git a/providers/aimlapi/models/anthropic/claude-sonnet-4.5.toml b/providers/aimlapi/models/anthropic/claude-sonnet-4.5.toml new file mode 100644 index 00000000000..6ebb192f07e --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-sonnet-4.5.toml @@ -0,0 +1,10 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "anthropic/claude-sonnet-4-5" +name = "Claude 4.5 Sonnet" +reasoning_options = [] + +[cost] +input = 4.1262 +output = 20.631 +cache_read = 0.41262 diff --git a/providers/aimlapi/models/anthropic/claude-sonnet-4.6.toml b/providers/aimlapi/models/anthropic/claude-sonnet-4.6.toml new file mode 100644 index 00000000000..a929c1d796d --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-sonnet-4.6.toml @@ -0,0 +1,14 @@ +base_model = "anthropic/claude-sonnet-4-6" +name = "Claude 4.6 Sonnet" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "max"] + +[cost] +input = 4.1262 +output = 20.631 +cache_read = 0.41262 + +[limit] +context = 200_000 diff --git a/providers/aimlapi/models/anthropic/claude-sonnet-5.toml b/providers/aimlapi/models/anthropic/claude-sonnet-5.toml new file mode 100644 index 00000000000..5aa47e77359 --- /dev/null +++ b/providers/aimlapi/models/anthropic/claude-sonnet-5.toml @@ -0,0 +1,13 @@ +base_model = "anthropic/claude-sonnet-5" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high", "xhigh", "max"] + +[cost] +input = 2.6 +output = 13 +cache_read = 0.26 diff --git a/providers/aimlapi/models/bytedance-seed/seed-2.0-code.toml b/providers/aimlapi/models/bytedance-seed/seed-2.0-code.toml new file mode 100644 index 00000000000..7686d164300 --- /dev/null +++ b/providers/aimlapi/models/bytedance-seed/seed-2.0-code.toml @@ -0,0 +1,13 @@ +base_model = "bytedance-seed/seed-2.0-code" +name = "Seed-2.0-Code" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.6877 +output = 4.1262 diff --git a/providers/aimlapi/models/bytedance-seed/seed-2.0-lite.toml b/providers/aimlapi/models/bytedance-seed/seed-2.0-lite.toml new file mode 100644 index 00000000000..ef3d8d0d0b7 --- /dev/null +++ b/providers/aimlapi/models/bytedance-seed/seed-2.0-lite.toml @@ -0,0 +1,17 @@ +base_model = "bytedance-seed/seed-2.0-lite" +name = "Seed-2.0-Lite" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.34385 +output = 2.7508 + +[limit] +context = 262_144 +output = 131_072 diff --git a/providers/aimlapi/models/bytedance-seed/seed-2.0-mini.toml b/providers/aimlapi/models/bytedance-seed/seed-2.0-mini.toml new file mode 100644 index 00000000000..44bd5bec985 --- /dev/null +++ b/providers/aimlapi/models/bytedance-seed/seed-2.0-mini.toml @@ -0,0 +1,17 @@ +base_model = "bytedance-seed/seed-2.0-mini" +name = "Seed-2.0-Mini" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.13754 +output = 0.55016 + +[limit] +context = 262_144 +output = 131_072 diff --git a/providers/aimlapi/models/cohere/command-r-08-2024.toml b/providers/aimlapi/models/cohere/command-r-08-2024.toml new file mode 100644 index 00000000000..78df1b3c45e --- /dev/null +++ b/providers/aimlapi/models/cohere/command-r-08-2024.toml @@ -0,0 +1,6 @@ +base_model = "cohere/command-r-08-2024" +name = "Command R (08-2024)" + +[cost] +input = 0.20631 +output = 0.82524 diff --git a/providers/aimlapi/models/cohere/command-r-plus-08-2024.toml b/providers/aimlapi/models/cohere/command-r-plus-08-2024.toml new file mode 100644 index 00000000000..38ee6a60d77 --- /dev/null +++ b/providers/aimlapi/models/cohere/command-r-plus-08-2024.toml @@ -0,0 +1,6 @@ +base_model = "cohere/command-r-plus-08-2024" +name = "Command R+ (08-2024)" + +[cost] +input = 3.4385 +output = 13.754 diff --git a/providers/aimlapi/models/cohere/command-r7b-12-2024.toml b/providers/aimlapi/models/cohere/command-r7b-12-2024.toml new file mode 100644 index 00000000000..3f8f98173fd --- /dev/null +++ b/providers/aimlapi/models/cohere/command-r7b-12-2024.toml @@ -0,0 +1,6 @@ +base_model = "cohere/command-r7b-12-2024" +name = "Command R7B (12-2024)" + +[cost] +input = 0.051578 +output = 0.20631 diff --git a/providers/aimlapi/models/deepseek/deepseek-chat.toml b/providers/aimlapi/models/deepseek/deepseek-chat.toml new file mode 100644 index 00000000000..a40e9780b00 --- /dev/null +++ b/providers/aimlapi/models/deepseek/deepseek-chat.toml @@ -0,0 +1,15 @@ +# Alias: answers "model" as deepseek-v4-flash; measured through that id. +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "deepseek/deepseek-v4-flash" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "high"] + +[cost] +input = 0.39 +output = 1.56 +cache_read = 0.0078 diff --git a/providers/aimlapi/models/deepseek/deepseek-v4-flash-vision-exp.toml b/providers/aimlapi/models/deepseek/deepseek-v4-flash-vision-exp.toml new file mode 100644 index 00000000000..a9e9cfd5795 --- /dev/null +++ b/providers/aimlapi/models/deepseek/deepseek-v4-flash-vision-exp.toml @@ -0,0 +1,17 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "deepseek/deepseek-v4-flash-vision-exp" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "high"] + +[cost] +input = 0.572 +output = 1.716 +cache_read = 0.0182 + +[limit] +context = 1_048_576 diff --git a/providers/aimlapi/models/deepseek/deepseek-v4-flash.toml b/providers/aimlapi/models/deepseek/deepseek-v4-flash.toml new file mode 100644 index 00000000000..641462df08f --- /dev/null +++ b/providers/aimlapi/models/deepseek/deepseek-v4-flash.toml @@ -0,0 +1,14 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "deepseek/deepseek-v4-flash" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "high"] + +[cost] +input = 0.39 +output = 1.56 +cache_read = 0.0078 diff --git a/providers/aimlapi/models/deepseek/deepseek-v4-pro-0813.toml b/providers/aimlapi/models/deepseek/deepseek-v4-pro-0813.toml new file mode 100644 index 00000000000..1782cd21a30 --- /dev/null +++ b/providers/aimlapi/models/deepseek/deepseek-v4-pro-0813.toml @@ -0,0 +1,17 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "deepseek/deepseek-v4-pro-0813" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["none", "high"] + +[cost] +input = 2.393196 +output = 4.786392 +cache_read = 0.199433 + +[limit] +context = 1_048_576 diff --git a/providers/aimlapi/models/deepseek/deepseek-v4-pro.toml b/providers/aimlapi/models/deepseek/deepseek-v4-pro.toml new file mode 100644 index 00000000000..5d101e0c3bc --- /dev/null +++ b/providers/aimlapi/models/deepseek/deepseek-v4-pro.toml @@ -0,0 +1,14 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "deepseek/deepseek-v4-pro" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["none", "high"] + +[cost] +input = 0.5655 +output = 1.131 +cache_read = 0.004713 diff --git a/providers/aimlapi/models/google/gemini-3.1-flash-lite-preview.toml b/providers/aimlapi/models/google/gemini-3.1-flash-lite-preview.toml new file mode 100644 index 00000000000..fca57f817b8 --- /dev/null +++ b/providers/aimlapi/models/google/gemini-3.1-flash-lite-preview.toml @@ -0,0 +1,16 @@ +base_model = "google/gemini-3.1-flash-lite-preview" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.34385 +output = 2.0631 +cache_read = 0.034385 + +[limit] +context = 1_000_000 diff --git a/providers/aimlapi/models/google/gemini-3.1-pro-preview-customtools.toml b/providers/aimlapi/models/google/gemini-3.1-pro-preview-customtools.toml new file mode 100644 index 00000000000..f4465ebdf4e --- /dev/null +++ b/providers/aimlapi/models/google/gemini-3.1-pro-preview-customtools.toml @@ -0,0 +1,16 @@ +base_model = "google/gemini-3.1-pro-preview-customtools" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 2.7508 +output = 16.5048 +cache_read = 0.27508 + +[limit] +context = 1_048_756 diff --git a/providers/aimlapi/models/google/gemini-3.1-pro-preview.toml b/providers/aimlapi/models/google/gemini-3.1-pro-preview.toml new file mode 100644 index 00000000000..14fc3cf4e4f --- /dev/null +++ b/providers/aimlapi/models/google/gemini-3.1-pro-preview.toml @@ -0,0 +1,12 @@ +# No caller control: reasoning_effort is not read (an invalid value returns 200), +# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200). +base_model = "google/gemini-3.1-pro-preview" +reasoning_options = [] + +[cost] +input = 2.6 +output = 15.6 +cache_read = 0.65 + +[limit] +context = 1_000_000 diff --git a/providers/aimlapi/models/google/gemini-3.6-flash.toml b/providers/aimlapi/models/google/gemini-3.6-flash.toml new file mode 100644 index 00000000000..cbb95c0086e --- /dev/null +++ b/providers/aimlapi/models/google/gemini-3.6-flash.toml @@ -0,0 +1,10 @@ +base_model = "google/gemini-3.6-flash" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] + +[cost] +input = 1.95 +output = 9.75 +cache_read = 0.195 diff --git a/providers/aimlapi/models/google/gemini-3.7-flash.toml b/providers/aimlapi/models/google/gemini-3.7-flash.toml new file mode 100644 index 00000000000..ca994263e13 --- /dev/null +++ b/providers/aimlapi/models/google/gemini-3.7-flash.toml @@ -0,0 +1,10 @@ +base_model = "google/gemini-3.7-flash" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.975 +output = 4.875 +cache_read = 0.0975 diff --git a/providers/aimlapi/models/google/gemini-flash-latest.toml b/providers/aimlapi/models/google/gemini-flash-latest.toml new file mode 100644 index 00000000000..99fc46d49b8 --- /dev/null +++ b/providers/aimlapi/models/google/gemini-flash-latest.toml @@ -0,0 +1,13 @@ +base_model = "google/gemini-flash-latest" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 2.0631 +output = 12.3786 +cache_read = 0.20631 diff --git a/providers/aimlapi/models/google/gemma-4-26b-a4b-it.toml b/providers/aimlapi/models/google/gemma-4-26b-a4b-it.toml new file mode 100644 index 00000000000..19c7b65f3a2 --- /dev/null +++ b/providers/aimlapi/models/google/gemma-4-26b-a4b-it.toml @@ -0,0 +1,12 @@ +# No caller control: reasoning_effort is not read (an invalid value returns 200), +# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200). +base_model = "google/gemma-4-26b-a4b-it" +reasoning_options = [] + +[cost] +input = 0.20631 +output = 0.82524 +cache_read = 0.20631 + +[limit] +output = 128_000 diff --git a/providers/aimlapi/models/google/gemma-4-31b-it.toml b/providers/aimlapi/models/google/gemma-4-31b-it.toml new file mode 100644 index 00000000000..47a3c16d39d --- /dev/null +++ b/providers/aimlapi/models/google/gemma-4-31b-it.toml @@ -0,0 +1,14 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "google/gemma-4-31b-it" +name = "Gemma 4 31B Instruct" +reasoning_options = [] + +[cost] +input = 1.361646 +output = 2.049346 +cache_read = 1.361646 + +[limit] +context = 256_000 +output = 16_384 diff --git a/providers/aimlapi/models/meta/muse-glimmer-30b.toml b/providers/aimlapi/models/meta/muse-glimmer-30b.toml new file mode 100644 index 00000000000..25a525a4ded --- /dev/null +++ b/providers/aimlapi/models/meta/muse-glimmer-30b.toml @@ -0,0 +1,13 @@ +base_model = "meta/muse-glimmer-30b" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.48139 +output = 2.0631 +cache_read = 0.055016 diff --git a/providers/aimlapi/models/meta/muse-spark-1.1.toml b/providers/aimlapi/models/meta/muse-spark-1.1.toml new file mode 100644 index 00000000000..edb4ee9b056 --- /dev/null +++ b/providers/aimlapi/models/meta/muse-spark-1.1.toml @@ -0,0 +1,13 @@ +base_model = "meta/muse-spark-1.1" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 1.71925 +output = 5.84545 +cache_read = 0.20631 diff --git a/providers/aimlapi/models/meta/muse-spark-1.2.toml b/providers/aimlapi/models/meta/muse-spark-1.2.toml new file mode 100644 index 00000000000..049015208c0 --- /dev/null +++ b/providers/aimlapi/models/meta/muse-spark-1.2.toml @@ -0,0 +1,13 @@ +base_model = "meta/muse-spark-1.2" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 1.71925 +output = 5.84545 +cache_read = 0.20631 diff --git a/providers/aimlapi/models/mistralai/devstral-2512.toml b/providers/aimlapi/models/mistralai/devstral-2512.toml new file mode 100644 index 00000000000..f0bb6422d14 --- /dev/null +++ b/providers/aimlapi/models/mistralai/devstral-2512.toml @@ -0,0 +1,7 @@ +base_model = "mistral/devstral-2512" +name = "Devstral 2 2512" + +[cost] +input = 0.605176 +output = 3.02588 +cache_read = 0.060518 diff --git a/providers/aimlapi/models/mistralai/mistral-large-2512.toml b/providers/aimlapi/models/mistralai/mistral-large-2512.toml new file mode 100644 index 00000000000..b1b3cbc769f --- /dev/null +++ b/providers/aimlapi/models/mistralai/mistral-large-2512.toml @@ -0,0 +1,7 @@ +base_model = "mistral/mistral-large-2512" +name = "Large 3 2512" + +[cost] +input = 0.6877 +output = 2.0631 +cache_read = 0.06877 diff --git a/providers/aimlapi/models/mistralai/mistral-nemo.toml b/providers/aimlapi/models/mistralai/mistral-nemo.toml new file mode 100644 index 00000000000..f1ce595b79b --- /dev/null +++ b/providers/aimlapi/models/mistralai/mistral-nemo.toml @@ -0,0 +1,10 @@ +base_model = "mistral/mistral-nemo" +name = "Mistral NeMo" + +[cost] +input = 0.20631 +output = 0.233818 +cache_read = 0.20631 + +[limit] +output = 16_384 diff --git a/providers/aimlapi/models/mistralai/mistral-small-2603.toml b/providers/aimlapi/models/mistralai/mistral-small-2603.toml new file mode 100644 index 00000000000..1186ce660e0 --- /dev/null +++ b/providers/aimlapi/models/mistralai/mistral-small-2603.toml @@ -0,0 +1,14 @@ +base_model = "mistral/mistral-small-2603" +name = "Small 4" + +[[reasoning_options]] +type = "effort" +values = ["none", "high"] + +[cost] +input = 0.257888 +output = 1.03155 +cache_read = 0.020631 + +[limit] +context = 262_144 diff --git a/providers/aimlapi/models/moonshot/kimi-k3.toml b/providers/aimlapi/models/moonshot/kimi-k3.toml new file mode 100644 index 00000000000..081843bca0d --- /dev/null +++ b/providers/aimlapi/models/moonshot/kimi-k3.toml @@ -0,0 +1,13 @@ +base_model = "moonshotai/kimi-k3" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + +[cost] +input = 3.9 +output = 19.5 +cache_read = 0.39 diff --git a/providers/aimlapi/models/moonshotai/kimi-k2-thinking.toml b/providers/aimlapi/models/moonshotai/kimi-k2-thinking.toml new file mode 100644 index 00000000000..e2547928dc6 --- /dev/null +++ b/providers/aimlapi/models/moonshotai/kimi-k2-thinking.toml @@ -0,0 +1,12 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "moonshotai/kimi-k2-thinking" +reasoning_options = [] + +[cost] +input = 0.82524 +output = 3.4385 +cache_read = 0.20631 + +[limit] +output = 98_304 diff --git a/providers/aimlapi/models/openai/gpt-3.5-turbo.toml b/providers/aimlapi/models/openai/gpt-3.5-turbo.toml new file mode 100644 index 00000000000..993c07fdbcd --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-3.5-turbo.toml @@ -0,0 +1,9 @@ +base_model = "openai/gpt-3.5-turbo" +name = "GPT-3.5 Turbo" + +[cost] +input = 0.65 +output = 1.95 + +[limit] +context = 16_000 diff --git a/providers/aimlapi/models/openai/gpt-4-turbo.toml b/providers/aimlapi/models/openai/gpt-4-turbo.toml new file mode 100644 index 00000000000..d90a9e47eb8 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4-turbo.toml @@ -0,0 +1,5 @@ +base_model = "openai/gpt-4-turbo" + +[cost] +input = 13 +output = 39 diff --git a/providers/aimlapi/models/openai/gpt-4.1-mini.toml b/providers/aimlapi/models/openai/gpt-4.1-mini.toml new file mode 100644 index 00000000000..454fbbfbba1 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4.1-mini.toml @@ -0,0 +1,10 @@ +base_model = "openai/gpt-4.1-mini" +name = "GPT-4.1 Mini" + +[cost] +input = 0.52 +output = 2.08 +cache_read = 0.13 + +[limit] +context = 1_000_000 diff --git a/providers/aimlapi/models/openai/gpt-4.1-nano.toml b/providers/aimlapi/models/openai/gpt-4.1-nano.toml new file mode 100644 index 00000000000..162a49cc035 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4.1-nano.toml @@ -0,0 +1,10 @@ +base_model = "openai/gpt-4.1-nano" +name = "GPT-4.1 Nano" + +[cost] +input = 0.13 +output = 0.52 +cache_read = 0.0325 + +[limit] +context = 1_000_000 diff --git a/providers/aimlapi/models/openai/gpt-4.1.toml b/providers/aimlapi/models/openai/gpt-4.1.toml new file mode 100644 index 00000000000..e522294beb7 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4.1.toml @@ -0,0 +1,6 @@ +base_model = "openai/gpt-4.1" + +[cost] +input = 2.6 +output = 10.4 +cache_read = 0.65 diff --git a/providers/aimlapi/models/openai/gpt-4.toml b/providers/aimlapi/models/openai/gpt-4.toml new file mode 100644 index 00000000000..2977fbeb28b --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4.toml @@ -0,0 +1,9 @@ +base_model = "openai/gpt-4" + +[cost] +input = 39 +output = 78 +cache_read = 39 + +[limit] +context = 8_000 diff --git a/providers/aimlapi/models/openai/gpt-4o-2024-05-13.toml b/providers/aimlapi/models/openai/gpt-4o-2024-05-13.toml new file mode 100644 index 00000000000..bb1db38bebf --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4o-2024-05-13.toml @@ -0,0 +1,9 @@ +base_model = "openai/gpt-4o-2024-05-13" +name = "GPT-4o" + +[cost] +input = 6.5 +output = 19.5 + +[limit] +output = 16_384 diff --git a/providers/aimlapi/models/openai/gpt-4o-2024-08-06.toml b/providers/aimlapi/models/openai/gpt-4o-2024-08-06.toml new file mode 100644 index 00000000000..ccee26a9c14 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4o-2024-08-06.toml @@ -0,0 +1,7 @@ +base_model = "openai/gpt-4o-2024-08-06" +name = "GPT-4o" + +[cost] +input = 3.25 +output = 13 +cache_read = 1.625 diff --git a/providers/aimlapi/models/openai/gpt-4o-2024-11-20.toml b/providers/aimlapi/models/openai/gpt-4o-2024-11-20.toml new file mode 100644 index 00000000000..e6424f40c0e --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4o-2024-11-20.toml @@ -0,0 +1,7 @@ +base_model = "openai/gpt-4o-2024-11-20" +name = "GPT-4o" + +[cost] +input = 3.25 +output = 13 +cache_read = 1.625 diff --git a/providers/aimlapi/models/openai/gpt-4o-mini.toml b/providers/aimlapi/models/openai/gpt-4o-mini.toml new file mode 100644 index 00000000000..30907a5f8e7 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4o-mini.toml @@ -0,0 +1,7 @@ +base_model = "openai/gpt-4o-mini" +name = "GPT-4o Mini" + +[cost] +input = 0.195 +output = 0.78 +cache_read = 0.0975 diff --git a/providers/aimlapi/models/openai/gpt-4o.toml b/providers/aimlapi/models/openai/gpt-4o.toml new file mode 100644 index 00000000000..88760223978 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-4o.toml @@ -0,0 +1,6 @@ +base_model = "openai/gpt-4o" + +[cost] +input = 3.25 +output = 13 +cache_read = 1.625 diff --git a/providers/aimlapi/models/openai/gpt-5-mini.toml b/providers/aimlapi/models/openai/gpt-5-mini.toml new file mode 100644 index 00000000000..936dc22c60e --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5-mini.toml @@ -0,0 +1,13 @@ +base_model = "openai/gpt-5-mini" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] + +[cost] +input = 0.378235 +output = 3.02588 +cache_read = 0.34385 diff --git a/providers/aimlapi/models/openai/gpt-5-nano.toml b/providers/aimlapi/models/openai/gpt-5-nano.toml new file mode 100644 index 00000000000..d038994a673 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5-nano.toml @@ -0,0 +1,13 @@ +base_model = "openai/gpt-5-nano" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] + +[cost] +input = 0.075647 +output = 0.605176 +cache_read = 0.06877 diff --git a/providers/aimlapi/models/openai/gpt-5-pro.toml b/providers/aimlapi/models/openai/gpt-5-pro.toml new file mode 100644 index 00000000000..53f5d36b39f --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5-pro.toml @@ -0,0 +1,12 @@ +base_model = "openai/gpt-5-pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["high"] + +[cost] +input = 20.631 +output = 165.048 diff --git a/providers/aimlapi/models/openai/gpt-5.1-codex-max.toml b/providers/aimlapi/models/openai/gpt-5.1-codex-max.toml new file mode 100644 index 00000000000..3abda7bb2ef --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.1-codex-max.toml @@ -0,0 +1,14 @@ +base_model = "openai/gpt-5.1-codex-max" +name = "GPT-5.1-Codex-Max" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 3.4385 +output = 27.508 +cache_read = 0.34385 diff --git a/providers/aimlapi/models/openai/gpt-5.1-codex-mini.toml b/providers/aimlapi/models/openai/gpt-5.1-codex-mini.toml new file mode 100644 index 00000000000..dccf0944ca6 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.1-codex-mini.toml @@ -0,0 +1,14 @@ +base_model = "openai/gpt-5.1-codex-mini" +name = "GPT-5.1 Codex Mini" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.34385 +output = 2.7508 +cache_read = 0.041262 diff --git a/providers/aimlapi/models/openai/gpt-5.1-codex.toml b/providers/aimlapi/models/openai/gpt-5.1-codex.toml new file mode 100644 index 00000000000..33b2d94cbe8 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.1-codex.toml @@ -0,0 +1,13 @@ +base_model = "openai/gpt-5.1-codex" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 3.4385 +output = 27.508 +cache_read = 0.34385 diff --git a/providers/aimlapi/models/openai/gpt-5.2-codex.toml b/providers/aimlapi/models/openai/gpt-5.2-codex.toml new file mode 100644 index 00000000000..5b16e1f8b2c --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.2-codex.toml @@ -0,0 +1,13 @@ +base_model = "openai/gpt-5.2-codex" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 4.8139 +output = 38.5112 +cache_read = 0.48139 diff --git a/providers/aimlapi/models/openai/gpt-5.2-pro.toml b/providers/aimlapi/models/openai/gpt-5.2-pro.toml new file mode 100644 index 00000000000..fb64e3c84fe --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.2-pro.toml @@ -0,0 +1,12 @@ +base_model = "openai/gpt-5.2-pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["medium", "high"] + +[cost] +input = 28.8834 +output = 231.0672 diff --git a/providers/aimlapi/models/openai/gpt-5.3-codex.toml b/providers/aimlapi/models/openai/gpt-5.3-codex.toml new file mode 100644 index 00000000000..a4f39525490 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.3-codex.toml @@ -0,0 +1,14 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "openai/gpt-5.3-codex" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 4.8139 +output = 38.5112 +cache_read = 0.48139 diff --git a/providers/aimlapi/models/openai/gpt-5.4-mini.toml b/providers/aimlapi/models/openai/gpt-5.4-mini.toml new file mode 100644 index 00000000000..5a493e14a40 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.4-mini.toml @@ -0,0 +1,11 @@ +base_model = "openai/gpt-5.4-mini" +name = "GPT-5.4 Mini" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 2.0631 +output = 12.3786 +cache_read = 0.20631 diff --git a/providers/aimlapi/models/openai/gpt-5.4-nano.toml b/providers/aimlapi/models/openai/gpt-5.4-nano.toml new file mode 100644 index 00000000000..a8f744792e0 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.4-nano.toml @@ -0,0 +1,11 @@ +base_model = "openai/gpt-5.4-nano" +name = "GPT-5.4 Nano" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 0.27508 +output = 1.71925 +cache_read = 0.027508 diff --git a/providers/aimlapi/models/openai/gpt-5.4-pro.toml b/providers/aimlapi/models/openai/gpt-5.4-pro.toml new file mode 100644 index 00000000000..9cbfce2f1cf --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.4-pro.toml @@ -0,0 +1,12 @@ +base_model = "openai/gpt-5.4-pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["medium", "high"] + +[cost] +input = 41.262 +output = 247.572 diff --git a/providers/aimlapi/models/openai/gpt-5.5-pro.toml b/providers/aimlapi/models/openai/gpt-5.5-pro.toml new file mode 100644 index 00000000000..1ed1ce270cc --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.5-pro.toml @@ -0,0 +1,12 @@ +base_model = "openai/gpt-5.5-pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["medium", "high"] + +[cost] +input = 41.262 +output = 247.572 diff --git a/providers/aimlapi/models/openai/gpt-5.6-luna-pro.toml b/providers/aimlapi/models/openai/gpt-5.6-luna-pro.toml new file mode 100644 index 00000000000..cbb21e9c66e --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-luna-pro.toml @@ -0,0 +1,14 @@ +base_model = "openai/gpt-5.6-luna" +name = "GPT-5.6 Luna Pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 0.27508 +output = 1.65048 +cache_read = 0.027508 diff --git a/providers/aimlapi/models/openai/gpt-5.6-luna.toml b/providers/aimlapi/models/openai/gpt-5.6-luna.toml new file mode 100644 index 00000000000..42e83f452c3 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-luna.toml @@ -0,0 +1,11 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "openai/gpt-5.6-luna" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 0.26 +output = 1.56 +cache_read = 0.026 diff --git a/providers/aimlapi/models/openai/gpt-5.6-sol-pro.toml b/providers/aimlapi/models/openai/gpt-5.6-sol-pro.toml new file mode 100644 index 00000000000..d350abe9f0b --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-sol-pro.toml @@ -0,0 +1,14 @@ +base_model = "openai/gpt-5.6-sol" +name = "GPT-5.6 Sol Pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 5.5016 +output = 27.508 +cache_read = 0.55016 diff --git a/providers/aimlapi/models/openai/gpt-5.6-sol.toml b/providers/aimlapi/models/openai/gpt-5.6-sol.toml new file mode 100644 index 00000000000..b5fefa47a8a --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-sol.toml @@ -0,0 +1,11 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "openai/gpt-5.6-sol" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 5.2 +output = 26 +cache_read = 0.52 diff --git a/providers/aimlapi/models/openai/gpt-5.6-terra-pro.toml b/providers/aimlapi/models/openai/gpt-5.6-terra-pro.toml new file mode 100644 index 00000000000..22cea416553 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-terra-pro.toml @@ -0,0 +1,15 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "openai/gpt-5.6-terra" +name = "GPT-5.6 Terra Pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 2.7508 +output = 16.5048 +cache_read = 0.27508 diff --git a/providers/aimlapi/models/openai/gpt-5.6-terra.toml b/providers/aimlapi/models/openai/gpt-5.6-terra.toml new file mode 100644 index 00000000000..c8e9ac4525e --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.6-terra.toml @@ -0,0 +1,11 @@ +# Off: reasoning_effort = none (0 reasoning tokens); no toggle field on this surface. +base_model = "openai/gpt-5.6-terra" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 2.6 +output = 15.6 +cache_read = 0.26 diff --git a/providers/aimlapi/models/openai/gpt-5.toml b/providers/aimlapi/models/openai/gpt-5.toml new file mode 100644 index 00000000000..5e43e7e65e0 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-5.toml @@ -0,0 +1,13 @@ +base_model = "openai/gpt-5" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["minimal", "low", "medium", "high"] + +[cost] +input = 1.891175 +output = 15.1294 +cache_read = 1.71925 diff --git a/providers/aimlapi/models/openai/gpt-oss-120b.toml b/providers/aimlapi/models/openai/gpt-oss-120b.toml new file mode 100644 index 00000000000..847a6b2c517 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-oss-120b.toml @@ -0,0 +1,17 @@ +base_model = "openai/gpt-oss-120b" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.48139 +output = 1.30663 +cache_read = 0.48139 + +[limit] +context = 131_000 +output = 117_964 diff --git a/providers/aimlapi/models/openai/gpt-oss-20b.toml b/providers/aimlapi/models/openai/gpt-oss-20b.toml new file mode 100644 index 00000000000..114b4f16097 --- /dev/null +++ b/providers/aimlapi/models/openai/gpt-oss-20b.toml @@ -0,0 +1,17 @@ +base_model = "openai/gpt-oss-20b" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.103155 +output = 0.41262 +cache_read = 0.051578 + +[limit] +context = 131_000 +output = 117_964 diff --git a/providers/aimlapi/models/openai/o1-pro.toml b/providers/aimlapi/models/openai/o1-pro.toml new file mode 100644 index 00000000000..e340c6100d3 --- /dev/null +++ b/providers/aimlapi/models/openai/o1-pro.toml @@ -0,0 +1,12 @@ +base_model = "openai/o1-pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 206.31 +output = 825.24 diff --git a/providers/aimlapi/models/openai/o1.toml b/providers/aimlapi/models/openai/o1.toml new file mode 100644 index 00000000000..151ea41109a --- /dev/null +++ b/providers/aimlapi/models/openai/o1.toml @@ -0,0 +1,13 @@ +base_model = "openai/o1" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 19.5 +output = 78 +cache_read = 9.75 + +[limit] +context = 128_000 diff --git a/providers/aimlapi/models/openai/o3-mini.toml b/providers/aimlapi/models/openai/o3-mini.toml new file mode 100644 index 00000000000..4790bb65f30 --- /dev/null +++ b/providers/aimlapi/models/openai/o3-mini.toml @@ -0,0 +1,11 @@ +base_model = "openai/o3-mini" +name = "o3 Mini" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 1.43 +output = 5.72 +cache_read = 0.715 diff --git a/providers/aimlapi/models/openai/o3-pro.toml b/providers/aimlapi/models/openai/o3-pro.toml new file mode 100644 index 00000000000..1e9022547c7 --- /dev/null +++ b/providers/aimlapi/models/openai/o3-pro.toml @@ -0,0 +1,13 @@ +base_model = "openai/o3-pro" +name = "o3 Pro" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 27.508 +output = 110.032 diff --git a/providers/aimlapi/models/perplexity/sonar-pro.toml b/providers/aimlapi/models/perplexity/sonar-pro.toml new file mode 100644 index 00000000000..d353019ac3a --- /dev/null +++ b/providers/aimlapi/models/perplexity/sonar-pro.toml @@ -0,0 +1,8 @@ +base_model = "perplexity/sonar-pro" + +[cost] +input = 3.9 +output = 19.5 + +[limit] +output = 100_000 diff --git a/providers/aimlapi/models/perplexity/sonar.toml b/providers/aimlapi/models/perplexity/sonar.toml new file mode 100644 index 00000000000..8b062038318 --- /dev/null +++ b/providers/aimlapi/models/perplexity/sonar.toml @@ -0,0 +1,8 @@ +base_model = "perplexity/sonar" + +[cost] +input = 1.3 +output = 1.3 + +[limit] +output = 100_000 diff --git a/providers/aimlapi/models/sakana/fugu-ultra.toml b/providers/aimlapi/models/sakana/fugu-ultra.toml new file mode 100644 index 00000000000..585cbf492e0 --- /dev/null +++ b/providers/aimlapi/models/sakana/fugu-ultra.toml @@ -0,0 +1,13 @@ +base_model = "sakana/fugu-ultra" + +[[reasoning_options]] +type = "effort" +values = ["high"] + +[cost] +input = 6.877 +output = 41.262 +cache_read = 0.6877 + +[limit] +output = 128_000 diff --git a/providers/aimlapi/models/tencent/hy3.toml b/providers/aimlapi/models/tencent/hy3.toml new file mode 100644 index 00000000000..47856496a42 --- /dev/null +++ b/providers/aimlapi/models/tencent/hy3.toml @@ -0,0 +1,18 @@ +base_model = "tencent/hy3" +base_model_omit = ["limit.input"] + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] + +[cost] +input = 0.27508 +output = 1.10032 +cache_read = 0.06877 + +[limit] +context = 262_144 +output = 131_072 diff --git a/providers/aimlapi/models/tencent/hy4-preview.toml b/providers/aimlapi/models/tencent/hy4-preview.toml new file mode 100644 index 00000000000..9448d93b7f1 --- /dev/null +++ b/providers/aimlapi/models/tencent/hy4-preview.toml @@ -0,0 +1,16 @@ +base_model = "tencent/hy4-preview" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "high"] + +[cost] +input = 1.147084 +output = 3.439875 +cache_read = 0.057767 + +[limit] +context = 1_048_576 diff --git a/providers/aimlapi/models/thinkingmachines/inkling-small.toml b/providers/aimlapi/models/thinkingmachines/inkling-small.toml new file mode 100644 index 00000000000..eb41ee89285 --- /dev/null +++ b/providers/aimlapi/models/thinkingmachines/inkling-small.toml @@ -0,0 +1,17 @@ +base_model = "thinkingmachines/inkling-small" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 0.797732 +output = 1.980576 +cache_read = 0.159546 + +[limit] +context = 524_288 +output = 262_144 diff --git a/providers/aimlapi/models/thinkingmachines/inkling.toml b/providers/aimlapi/models/thinkingmachines/inkling.toml new file mode 100644 index 00000000000..5ec8e4b621d --- /dev/null +++ b/providers/aimlapi/models/thinkingmachines/inkling.toml @@ -0,0 +1,13 @@ +base_model = "thinkingmachines/inkling" + +[interleaved] +field = "reasoning_details" + +[[reasoning_options]] +type = "effort" +values = ["low", "medium", "high"] + +[cost] +input = 1.3754 +output = 5.57037 +cache_read = 0.233818 diff --git a/providers/aimlapi/models/upstage/solar-pro4.toml b/providers/aimlapi/models/upstage/solar-pro4.toml new file mode 100644 index 00000000000..4db867cd117 --- /dev/null +++ b/providers/aimlapi/models/upstage/solar-pro4.toml @@ -0,0 +1,10 @@ +base_model = "upstage/solar-pro4" + +[[reasoning_options]] +type = "effort" +values = ["none", "low", "medium", "high"] + +[cost] +input = 0.041262 +output = 0.165048 +cache_read = 0.008252 diff --git a/providers/aimlapi/models/xiaomi/mimo-v2.5-pro.toml b/providers/aimlapi/models/xiaomi/mimo-v2.5-pro.toml new file mode 100644 index 00000000000..33df4830dda --- /dev/null +++ b/providers/aimlapi/models/xiaomi/mimo-v2.5-pro.toml @@ -0,0 +1,17 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "xiaomi/mimo-v2.5-pro" +name = "MiMo V2.5 Pro" +reasoning_options = [] + +[interleaved] +field = "reasoning_details" + +[cost] +input = 0.598299 +output = 1.196598 +cache_read = 0.004951 + +[limit] +context = 1_000_000 +output = 128_000 diff --git a/providers/aimlapi/models/xiaomi/mimo-v2.5.toml b/providers/aimlapi/models/xiaomi/mimo-v2.5.toml new file mode 100644 index 00000000000..185533b4a3c --- /dev/null +++ b/providers/aimlapi/models/xiaomi/mimo-v2.5.toml @@ -0,0 +1,17 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "xiaomi/mimo-v2.5" +name = "MiMo V2.5" +reasoning_options = [] + +[interleaved] +field = "reasoning_details" + +[cost] +input = 0.192556 +output = 0.385112 +cache_read = 0.003851 + +[limit] +context = 1_000_000 +output = 128_000 diff --git a/providers/aimlapi/models/z-ai/glm-4.5v.toml b/providers/aimlapi/models/z-ai/glm-4.5v.toml new file mode 100644 index 00000000000..4fd16eee24f --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-4.5v.toml @@ -0,0 +1,16 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "zhipuai/glm-4.5v" +name = "GLM 4.5V" +reasoning_options = [] + +[interleaved] +field = "reasoning_details" + +[cost] +input = 0.82524 +output = 2.47572 +cache_read = 0.151294 + +[limit] +context = 65_536 diff --git a/providers/aimlapi/models/z-ai/glm-4.6v.toml b/providers/aimlapi/models/z-ai/glm-4.6v.toml new file mode 100644 index 00000000000..973d13d224e --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-4.6v.toml @@ -0,0 +1,16 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "zhipuai/glm-4.6v" +name = "GLM 4.6V" +reasoning_options = [] + +[interleaved] +field = "reasoning_details" + +[cost] +input = 0.41262 +output = 1.23786 +cache_read = 0.075647 + +[limit] +context = 131_072 diff --git a/providers/aimlapi/models/z-ai/glm-4.7-flash.toml b/providers/aimlapi/models/z-ai/glm-4.7-flash.toml new file mode 100644 index 00000000000..88b3d72d30c --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-4.7-flash.toml @@ -0,0 +1,17 @@ +# No caller control: reasoning_effort is validated (invalid value -> 400) but low/high +# do not order reasoning tokens; toggle/budget fields are dropped by the gateway. +base_model = "zhipuai/glm-4.7-flash" +name = "GLM 4.7 Flash" +reasoning_options = [] + +[interleaved] +field = "reasoning_details" + +[cost] +input = 0.171925 +output = 0.6877 +cache_read = 0.013754 + +[limit] +context = 202_752 +output = 16_384 diff --git a/providers/aimlapi/models/z-ai/glm-5-turbo.toml b/providers/aimlapi/models/z-ai/glm-5-turbo.toml new file mode 100644 index 00000000000..e7bcb32d106 --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-5-turbo.toml @@ -0,0 +1,13 @@ +# No caller control: reasoning_effort is not read (an invalid value returns 200), +# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200). +base_model = "zhipuai/glm-5-turbo" +name = "GLM 5 Turbo" +reasoning_options = [] + +[cost] +input = 1.56 +output = 5.2 +cache_read = 0.312 + +[limit] +context = 262_144 diff --git a/providers/aimlapi/models/z-ai/glm-5.3-flash.toml b/providers/aimlapi/models/z-ai/glm-5.3-flash.toml new file mode 100644 index 00000000000..b17a00ac81b --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-5.3-flash.toml @@ -0,0 +1,17 @@ +base_model = "zhipuai/glm-5.3-flash" +name = "GLM 5.3 Flash" + +[interleaved] +field = "reasoning_content" + +[[reasoning_options]] +type = "effort" +values = ["low", "high", "max"] + +[cost] +input = 0.195 +output = 0.65 +cache_read = 0.039 + +[limit] +context = 1_048_576 diff --git a/providers/aimlapi/models/z-ai/glm-5v-turbo.toml b/providers/aimlapi/models/z-ai/glm-5v-turbo.toml new file mode 100644 index 00000000000..02230f0e612 --- /dev/null +++ b/providers/aimlapi/models/z-ai/glm-5v-turbo.toml @@ -0,0 +1,13 @@ +# No caller control: reasoning_effort is not read (an invalid value returns 200), +# and toggle/budget fields are dropped by the gateway (unknown top-level keys -> 200). +base_model = "zhipuai/glm-5v-turbo" +name = "GLM 5V Turbo" +reasoning_options = [] + +[cost] +input = 1.56 +output = 5.2 +cache_read = 0.312 + +[limit] +context = 202_752 diff --git a/providers/aimlapi/provider.toml b/providers/aimlapi/provider.toml new file mode 100755 index 00000000000..6b7a3f559b9 --- /dev/null +++ b/providers/aimlapi/provider.toml @@ -0,0 +1,5 @@ +name = "AI/ML API" +env = ["AIMLAPI_API_KEY"] +npm = "@ai-sdk/openai-compatible" +api = "https://api.aimlapi.com/v1" +doc = "https://docs.aimlapi.com"