Conversation
`CostTier` extends `Cost`, so `input_audio` and `output_audio` are valid on a tier, but `formatToml` only emitted them for the top-level `[cost]` table. Any sync that rewrote a model with tiered audio rates silently dropped them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AIHubMix has had no sync module, so its 77 models were only ever refreshed by hand-written PRs. The last one landed 2026-08-31, which is why prices have drifted and new relays never arrive on their own. The endpoint (`https://aihubmix.com/api/v1/models?type=llm`, no auth) is authoritative for pricing and deprecation status only, matching the Ofox scope. Token limits and modalities are deliberately not synced: the endpoint reports each relay's conservative defaults rather than the upstream model's capabilities. It caps `context_length` per relay (Claude Opus 4.6 is listed at 200K against its 1M window), quotes `max_output` per default request, and never lists `pdf` even for models that accept PDFs. `cache_read` is ignored when it equals `input`: the endpoint echoes the input price for models with no cached rate configured, which covers 35 of the 301 priced entries at a nonzero price (plus 51 free models reporting 0 across the board, where the guard is a no-op). Taking the echoed value literally would have set Gemini 3.1 Flash Lite to $0.25 against the $0.025 that 26 other providers list. The first run updates 17 models. Beyond precision refinements it corrects real drift: GPT-5.6 Luna to OpenAI's own $0.20/$1.20 (was $1/$6), Sol and Terra to their current cuts, Gemini 3.5 Flash's `cache_read` from $1.50 to $0.15 (the authored value had the same echoed-input bug), and the Coding MiMo v2.5 output rates onto Xiaomi's actual 2:1 ratio. New relays are not created automatically (`skipCreates`) since AIHubMix serves roughly 400 upstream models against this hand-verified subset; each missing ID opens a deduped issue instead. Routing aliases such as `alicloud-glm-5.1` are served but unlisted, so local files absent from the response are retained. The Gemini 2.5 Flash thinking-budget comment moves to the file header, which is the only comment block `formatToml` preserves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AIHubMix now serves capabilities, limits, modalities and reasoning controls alongside pricing, so the adapter reads all of them instead of treating the endpoint as authoritative for cost and status alone. Relays are factored onto the lab metadata they serve: `developer_id` maps a relay to its lab, and routing prefixes (`coding-`, `alicloud-`) and suffixes (`-free`, `-think`, `-nothink`) select a mode rather than a different model, so they are stripped when resolving the base. A relay then records only what it actually changes. With bases resolving, new IDs no longer need to be held back, so `skipCreates` is dropped and 155 relays are created. Three source quirks are handled in translation rather than written through: `reasoning_options[]` carries an AIHubMix-only `default` key the strict schema rejects, two effort levels are spelled `no_think` and `instant`, and `max_output: 0` means "unknown" rather than a real ceiling for 102 of 415 models. A relay with neither resolvable lab metadata nor the release_date and open_weights a standalone entry requires is reported rather than written with invented values. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
AIHubMix lowercases every relay ID while labs keep their own casing, so `minimax-m2` never matched `minimax/MiniMax-M2` and the whole MiniMax line fell through to the standalone path. The lab index is now case-folded, and `nvidia-`/`bai-` join the routing prefixes with `-highspeed`, `-fast` and `-latest` joining the suffixes. 24 relays that previously had no resolvable base now factor onto one. The endpoint signals an unknown output ceiling three ways: 0, the value of `context_length` (51 of 415 models, which would leave no room for the prompt), and a value above the window (6 models, up to 10x). All three are read as absent so the base model's real ceiling shows through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cc-`, `mm-`, `aihubmix-`, `aihub-` and `ahm-` are AIHubMix's own namespaces, and `cloudflare-`/`deepinfra-` name the upstream compute a relay routes to, the same way `alicloud-` already did. Stripping them resolves 16 more relays onto the lab metadata they serve. `cc-minimax-m2` and `cc-MiniMax-M2` are one route under two spellings and would claim filenames differing only in case, so the response is deduplicated on the folded ID, keeping the last record whole. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
AIHubMix fixed 14 routes that had quoted `max_output` equal to `context_length`, plus two `context_length` values rounded to 131_000. Every corrected value matches what the other providers in the catalog already record for the same model. Nine files change and all nine shrink: the endpoint now agrees with the lab metadata, so the factored entries stop recording an override. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
AIHubMix pins snapshot dates onto relay IDs (`gemini-2.5-pro-preview-06-05`) while labs name the model itself (`google/gemini-2.5-pro`), so the tag has to come off before the ID can match. Peel routing and date affixes to a fixed point instead of one pass per rule, since they stack — `coding-gemini-2.5-pro- preview-05-06-search` carries three, with the date wedged between two of them. The date patterns are anchored and validate real month and day ranges so `llama2-70b-4096` keeps its context size and `-13-45` stays attached to nothing. The unstripped ID is still tried first, so a lab that genuinely carries a date in its name (`cohere/command-a-03-2025`) still wins. Also map developer_id 34 (muse-spark) and 35 (laguna) to the labs that publish them. 36, 37, 43 and 25 have no lab directory in models/ at all, so mapping them would not resolve anything. 27 relays now resolve to a base model: 299 of 415 source models covered, up from 272. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notes predated case-folded lookups, vanity prefixes, the second limit sentinel and the date-tag rules, and quoted counts from an older snapshot of the endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
This reverts commit 5682db7.
Follows the revert: a dated snapshot is its own model, and jiekou, nano-gpt, kilo and openrouter all write those IDs standalone rather than factoring them onto the undated lab entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
…ant_of AIHubMix now serves `vendor`, `variant_of` and `open_weights`, so nothing about a relay has to be inferred from its ID or mirrored in this repo any more. - `vendor` replaces the hand-maintained `developer_id` table. `VENDOR_LABS` is all that is left of it: the four labs the two registries spell differently. - `variant_of` replaces the routing prefix/suffix lists. A relay is looked up under its own ID first and then under each declared hop, nearest first, so `qwen3.8-max-preview` factors onto the preview rather than its chain root. Following the declared chain also reaches relays no string rule could — `ox-alpha` onto `zhipuai/glm-5.3-flash`, `grok-code-fast-1` onto `xai/grok-build-0.1`, `cohere-command-a` onto `cohere/command-a-03-2025`. - `open_weights` is served for 289 of 408 models, which unblocks standalone creates. A standalone entry also needs limits, so the skip guard now checks them; without it the endpoint's 0-output models fail catalog validation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pull request carried 299 generated `providers/aihubmix/models` files alongside the adapter, which pushed the diff past GitHub's 300-file ceiling. `gh pr diff --patch` then answered HTTP 406, and the reviewer workflow died in its context step before the agent ever ran -- so no revision of this branch could earn `reviewer: ready`. Ship the adapter alone. Once it merges, `sync-models.yml` regenerates the catalog on `automation/sync-models-aihubmix`, a branch the reviewer workflow already skips by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reviewer workflow has not seen this branch since 10 SepFlagging a mechanical problem rather than a review finding, because it silently swallows every revision pushed here.
This PR carried 304 files, and that call answers:
Two things follow. On our side — the PR is now 4 files. I have dropped the 299 generated The catalog belongs to the automation anyway: once this merges, On yours — the workflow is worth hardening. Any PR touching a large provider will hit this, and it fails in the least helpful way: red check, no comment, no label, no indication that the reviewer was never invoked. Your own error message names both fixes; the checkout is already there, so the local one is close at hand: Worth considering separately: at minimum, a distinct failure message so "the reviewer could not read this PR" stops looking like "the reviewer found problems". On the
|
Correcting myself on
|
| request | reasoning chars | reasoning tokens |
|---|---|---|
reasoning_effort: "none" |
0 | – |
enable_thinking: false |
0 | – |
| both together | 0 | – |
reasoning_effort: "minimal" |
217 | 64 |
reasoning_effort: "max" |
328 | 92 |
enable_thinking: true |
384 | 117 |
reasoning_effort: "turbo" (not a real tier) |
302 | 89 |
Three things fall out. Both off-switches genuinely turn thinking off, independently. Supplying both is coherent rather than contradictory — no error, no conflict, the same off state. And an unrecognised effort value returns 200 having been mapped to a sensible tier instead of 400 — which is the design intent stated plainly: accommodate the dialect the caller happens to speak, do not punish them for it.
So the endpoint is not reporting a malformed capability. It is reporting a wider control surface than a first-party model has, because a relay genuinely has one.
That leaves a real question about your catalog rather than about our data, and it is yours to settle. The AGENTS.md rule reads naturally for a first-party model, where one control surface exists and a second is evidence of a mistake. For a relay in front of many labs, "both dialects are accepted" is simply true, and the current schema has no way to say it — so it comes out looking like the anti-pattern. If you would rather the catalog carry the narrower first-party shape, say which of the two you want and I will make the adapter emit that consistently; I would just rather not have it recorded as a bug we are fixing, because it is neither.
One thing I still agree with from the earlier reviews, for the avoidance of doubt: I would not want the adapter intersecting effort values against per-model lab baselines. That is hand-maintained model knowledge in a code path whose whole purpose is to not have any, and it goes stale the moment an upstream adds a tier.
A note for later, not a request now
Once the adapter is reviewed and merged and the catalog has synced a few times, I would like to follow the path #4840, #4668, #4849 and #5078 took and ask for aihubmix in REVIEWED_REASONING_PROVIDERS. Not yet — that trust should be earned by the sync output looking right for a while first, and I would rather it be your call on evidence than a favour bundled into this PR. Flagging the intent so it is not a surprise.
🤖 Generated with Claude Code
Action items
|
A sync rewrites the model file whole, so any header a human wrote on it is lost the first time the model changes. AIHubMix reaches the same thinking toggle from four dialects -- `enable_thinking` on the OpenAI-compatible path, `thinking.type` on `/v1/messages`, `generationConfig.thinkingConfig` on the Gemini path -- so `toggle` alone does not tell a caller which field to send. Emit the header from `translateModel` whenever the model carries a toggle, the way the OpenRouter adapter already does, so the wire path survives the rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
The catalog endpoint carries text and cache rates only -- no audio and no reasoning rate, at the top level or inside a tier. `buildCost` rebuilt the cost object from that answer alone, so the next sync would have wiped the `input_audio` already authored on `doubao-seed-2-0-lite`, `doubao-seed-2-0-mini` and `gemini-2.5-flash`. Carry the authored audio and reasoning rates through, matching tiers by context size, the way the other gateway adapters do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AIHubMix accepts whichever off switch the caller's SDK speaks -- `reasoning_effort: "none"` on the Chat path, `enable_thinking: false`, `thinking.type: "disabled"` on `/v1/messages`, `thinkingBudget: 0` on the Gemini path -- and maps each onto the vendor's real control instead of rejecting it. 34 of 408 routes therefore publish both a toggle and a graded effort list carrying `none`. The catalog spells that one way: `AGENTS.md` says graded effort that already includes `none` stands alone, with no toggle. Fold it, and name the dialects that reach the same off state in the file header, which is where that belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
The endpoint has no surface for several things a provider file carries,
and the adapter rebuilt each object from the endpoint answer alone, so
the first automation run would have dropped them:
- `experimental` and `provider` -- the `[experimental.modes.fast]` block
and its nested request body on `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`
- `limit.input` -- the 922k input cap the catalog models and AIHubMix
does not
- reasoning budget bounds -- the endpoint states that a budget exists
but never its range, so a bare `{ type = "budget_tokens" }` written
onto a `base_model` file would override the lab's real `min`/`max`
with an unbounded control
Carry all four through from the authored file, the way the Anthropic,
OpenRouter and Merge Gateway adapters do.
Six models and four context tiers also repeat the input price in
`cache_read`, which is how the endpoint spells "no cache discount"
rather than a real rate; an omitted field already means "no such rate"
here, so an echoed one is now read the same way instead of publishing a
full-price read as a 10x discount.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The catalog under-reports what a route accepts. It lists `text,image` for `kimi-k2.5`, whose lab entry and this repo both record video, and `text` for `qwen3.8-2.4t-a95b`, whose own file carries a note that live image input returned 200 on 2026-08-31. Treating the endpoint as authoritative would delete both on the first sync. Union the endpoint list with what the file recorded instead. A modality the endpoint adds still lands; one it never listed is removed by editing the file, which is where it came from. Both gaps are reported upstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
The endpoint never sends `false` for a capability it does not know: 107 of 408 routes omit `reasoning` and 100 omit `tool_call`, and no route sends `false` at all. Reading a missing flag as `false` wrote an override that disabled a reasoner the lab entry declares. Modalities had the same shape of bug one level down. The union added in the previous commit merged the endpoint's list with the existing file, but `dev` carries only 77 aihubmix files, so most of the catalog arrives as a create with no file to merge against — 14 creates in the current listing would have written a narrowing override (`gpt-4o` losing pdf, `qwen3.5-27b` losing audio). The union now also includes the lab entry the relay factors onto. Headers were retained rather than refreshed, so a wire path could outlive the options it documents and a folded toggle kept advertising a toggle. `authoritativeHeaders` fixes that but would have deleted the price citations and live-test records humans wrote in the same block, so translateModel now reads the existing header and supersedes only the wire-path lines it authors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
Four review findings, all fixed at the adapter's shape rules rather than with per-model tables. - Limits no longer publish a decimal restatement of a binary window as a narrowing override. A stated limit below an accepted one but at or above 1000³/1024³ resolves to the accepted value; genuine host caps still land. 25 narrowing overrides become 7, and three MiniMax restatements an earlier sync wrote into files are retired. - A full standalone entry is only authored where the response names no vendor. A named lab means the relay belongs on base_model, so 38 would-be standalone creates for lab models become skips that name the file a human must add. - SyncProvider gains trackMissingModels, so a provider that creates models but still skips the ones it cannot write opens deduped [missing-model] issues instead of notices nobody acts on. - budget_tokens bounds cannot be copied from a lab entry: ModelMetadata has no reasoning_options field, so there is no such baseline to shadow. The comment records why rather than adding a fallback that could never fire. Also drops eight channel-alias files (alicloud-glm-5.1, zai-glm-5.1, the four deepseek-v4 channel routes, two xiaomi-mimo-v2.5 routes). Each relays to a model already in the catalog and echoes that model's ID back; the endpoint's main model list, not callability, is the catalog boundary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Action items
|
…an notes Limits - The unit-restatement test now compares a ratio instead of a direction. The endpoint restates 204800 as 200000 and 1000000 as 1048576, and neither is the host stating a different window; checking only the narrowing side left 20 routes writing an override that states no difference at all. - The accepted value is looked for in the lab entry first and only then in the provider file, so a restatement resolves to the spelling that makes the override disappear. Resolving file-first pinned 10 imprecise numbers forever (qwen3.7-flash's 991000 for the lab's 1000000). - Whatever the restatement resolves to is clamped to the lab's window, after the resolution rather than instead of it: a relay cannot serve a wider window than the model it relays, and an endpoint quoting back the file's own stale ceiling is only caught by a later clamp (grok-4.5 held 1000000 against a lab 500000). Header - The two wire-path lines this adapter owns are dropped whether or not a derived block replaces them. Keeping them when nothing is derived left a route advertising a toggle it no longer had, and no later sync could tell. - A superseded line is recognised by its opening on the trimmed line, so an indented ` # Toggle:` no longer outlives its block. - Eight provider files carried human notes mid-body, which a sync drops; moved above the first key as AGENTS.md requires. Names - The endpoint label is compared on the bare ID, which is what resolved the base model, so 10 namespaced routes stop taking a redundant storefront override. - A blank label is skipped rather than written: ModelBase.name is min(1), and writing one through aborted the whole provider's sync at validation. normalizeModelSlug is exported from openrouter.ts, which already serves as the shared helper module for the other provider adapters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Closed in favour of a clean PR: #7042. Same branch, same head commit — the history here carried 299 generated catalog files through several revisions, which is what kept All four findings from the review are addressed there, including the one about narrowing |
Action items
|
Adds a fully interface-driven sync adapter for AIHubMix, an aggregator that
relays ~408 LLM routes from 26 model labs. Everything is read from the public
catalog endpoint — no credentials, no per-model overrides in the adapter.
This PR ships the adapter only
An earlier revision carried the 299 generated
providers/aihubmix/models/*.tomlfiles alongside the adapter. That pushed the diff past GitHub's 300-file ceiling,
gh pr diff --patchansweredHTTP 406, andpr-reviewer.ymldied in itscontext step before the agent ever started — so no revision of the branch could
reach a review at all.
The catalog files are now dropped from the PR. Once the adapter merges,
sync-models.ymlregenerates them onautomation/sync-models-aihubmix, a branchthe reviewer workflow already skips by design. The diff here is 13 files: the
adapter, three framework/test files,
sync.md, and eight stale alias files thisrevision removes (below).
Answers to the review findings
togglealongside an effort list containingnone— folded, perAGENTS.md34 of 408 routes publish both
{type = "toggle"}and a graded effort listcontaining
none. That is not a defect in the source: AIHubMix is amulti-dialect gateway, and it accepts whichever off switch the caller's SDK
speaks, mapping each onto the vendor's real control rather than erroring.
providers/aihubmix/provider.tomlondevalready documents all four paths:$.reasoning_effort = "none"|"minimal"|…|"xhigh";"none"disables$.thinking.type = "enabled"|"disabled"|"adaptive"$.generationConfig.thinkingConfig,thinkingBudget: 0off$.reasoning.effortVerified live against
glm-5.2on 2026-09-11 with a fixed prompt: bothreasoning_effort: "none"andenable_thinking: falseindependently produce 0reasoning characters, sending both together is coherent rather than an error,
minimal(217 chars) andmax(328) are distinct live tiers — and a bogusreasoning_effort: "turbo"returns HTTP 200 mapped to a real tier, not 400.But the catalog spells this one way, and
AGENTS.mdis explicit: graded effortthat already carries
nonestands alone, with notoggle. That is a shaperule applied uniformly — not per-model knowledge — so the adapter now folds it,
and the dialects that reach the same off state are named in the file header,
which is where
AGENTS.mdsays they belong:A toggle whose effort list has no off value is untouched —
{ effort = ["high", "max"] } + togglestill writes both. Regression tests cover both shapes,including the
no_think→nonealias path.(An earlier comment of mine on this PR said this shape "is being fixed at the
source". That was my own inference and it was wrong; it is documented, intended
gateway behaviour. The catalog-side normalisation above is the right place to
reconcile it.)
Wire path for the toggle — fixed
The reviewer was right that
{type = "toggle"}on its own does not tell a callerwhich field to send, and that a sync rewrites the file whole and would erase a
hand-written header.
translateModelnow emits the header itself whenever themodel carries a reasoning control — the same shape the OpenRouter adapter uses —
so the wire path survives every rewrite instead of depending on a human re-adding
it.
A sync must not delete what only the file records — fixed
The endpoint has no surface for several things a provider file carries, and the
adapter rebuilt each object from the endpoint answer alone. All of these now
carry through from the authored file, the way the Anthropic, OpenRouter and
Merge Gateway adapters do:
experimental,provider[experimental.modes.fast]block and its nested request body ongpt-5.4,gpt-5.4-mini,gpt-5.5limit.inputbudget_tokensmin/maxbase_modelfile would replace the lab's real bounds with an unbounded controlinput_audio,output_audio,cost.reasoningdoubao-seed-2-0routes andgemini-2.5-flashTwo more in the same family:
cache_read. 6 models and 4 context tiers repeat the input price incache_read. An omitted price field already means "no such rate" for thisendpoint, so an echoed one is now read the same way rather than published as a
discount it is not.
text,imageforkimi-k2.5, whose lab entry records video, andtextforqwen3.8-2.4t-a95b, whose own file notes a live 200 on image input. Theendpoint list is now unioned with what the file recorded — it can widen a
route, never silently narrow one. (58 of 60 existing files already match the
live list exactly;
pdfin particular is served now, 53 routes includinggpt-5.4astext,image,pdf. The two gaps are reported upstream.)A create must not override lab metadata off — fixed
The same class of bug one level deeper, and the more dangerous half of it:
devcarries 77 aihubmix files, so most of the catalog arrives as a createwith no file to fall back on. Anything read from
existingis simply absent onthe run that writes the other 180.
reasoning/tool_callfell back tofalsefalse: 107 of 408 routes omitreasoning, 100 omittool_call, 0 deny eitherundefined, so the lab value is inheritedgpt-4o,gpt-4.1,o3,claude-opus-4-1,gpt-5.5-pro,gpt-6-astralosingpdf; sixqwen3.5/qwen3.6routes losingaudio;mimo-v2-omnilosingpdftogglekept advertising a toggleauthoritativeHeaders: trueauthoritativeHeaderson its own would have deleted the price citations, sourcelinks and live-test records humans wrote into the same comment block — seven
files on
devcarry one.translateModelnow reads the existing header(one new optional accessor on the sync context) and supersedes only the
wire-path lines it authors; everything else is carried through verbatim.
Under-reported relay limits written as authoritative overrides — fixed
The reviewer was right and my earlier answer here was wrong. I had measured only
existingfiles against their lab entry and reported "two of 206"; the actualexposure is on the creates, where there is no file. Re-measured across every
entry the adapter writes: 25 narrowing
[limit]overrides, not two.They are not all the same thing, and that is the whole fix. Most are the endpoint
restating a binary window in decimal — eight
glmroutes quote 204800 as 200000,four quote 1048576 as 1000000 — which is not the host narrowing anything. A
handful are real host caps. A ratio separates them without any per-model table: a
decimal restatement of a binary window loses at most
1000/1024per K unit, so1000³/1024³(three nested unit swaps) is the floor of what a restatement canexplain. A stated limit below an accepted one but at or above that ratio resolves
to the accepted value and writes no override.
[limit]overrides writtenThe 18 retired: 8
glmroutes at 204800→200000 (0.977), 4 at 1048576→1000000(0.954),
mimo-v2-omni262144→256000, three MiniMax files an earlier sync hadalready written at 131072→128000/128100, and
qwen3.6-plus-preview-freeat65536→65535.
The 7 that survive are an order of magnitude away from a unit swap and are kept:
grok-code-fast-1caps output at 10000 of a 256000 window (0.039), four Kimiroutes at 32768 of 262144 (0.125),
gpt-5-chat-latestat 16384 of 128000 and a128000 context of the lab's 400000 (0.320).
Two details the measurement forced:
quote, which is what retires the three MiniMax restatements already sitting in
files on
dev.the lab's. An authored limit is not a stale copy of the lab's but a narrower one
on purpose —
kimi-k2.5serves 32768 of a 262144 window — and widening it tothe lab's would have deleted a real cap. My first attempt did exactly that.
A full standalone entry for a model a lab built — fixed
AGENTS.mdis explicit: if the provider did not create the model, the providerentry must use
base_model. The adapter honoured that whenever the lab entryexisted, but fell through to the standalone path when it did not — writing a
complete third-party TOML for, say, a DeepSeek model into
providers/aihubmix/.A standalone entry is now only authored where the response names no
vendor.A named lab means the relay belongs on
base_model, so the relay is skipped andreported for a human to add
models/<lab>/<id>.toml, after which it factors withno change to the adapter.
The 7 remaining are pre-existing files on
devbeing updated, not new creates.81 of 407 routes name a vendor whose lab entry does not exist yet — 38 of them
described completely enough that the endpoint answer alone would have satisfied
the standalone guard. Those 81 now open deduped
[missing-model]issues instead(next section).
A standalone file already in the repo keeps being updated rather than frozen:
what it should have been is upstream's call, and freezing it would only stall its
prices.
Skips that produced notices nobody acts on — fixed
skipCreatesis unset for aihubmix because creates do run, and the runner onlyfed
skippedRemoteinto the missing-model issue path whenskipCreates === true.So every unresolved relay produced a line in the sync report and nothing else.
SyncProvidergainstrackMissingModels?: boolean— implied byskipCreates,and settable on its own for exactly this case: a provider that creates most
models but still cannot write some of them. aihubmix sets it, and each skipped
relay now opens one deduped
[missing-model]issue naming the file to add.Blast radius checked:
ofoxandpioneerare the only other providers with theflag and both already set
skipCreates: true, so no existing provider'sbehaviour changes.
budget_tokensbounds copied from a lab or peer entry — cannot be doneThe suggestion was to copy
min/maxfrom the same model's first-party or peerprovider entry when the endpoint sends a bare budget. There is nowhere to copy
from, at the schema level:
ModelMetadata(packages/core/src/schema.ts)has no
reasoning_optionsfield at all, so amodels/<lab>/<model>.tomlentrycannot state a budget range. Measured across the current catalog: 0 of 370
models/entries carry areasoning_optionskey, and the only four files thatmention it at all do so in a comment saying cost and
reasoning_optionsare"intentionally left to the provider files".
That is also the right boundary rather than an accident. A budget range is a
property of the host's API — what AIHubMix will accept on the wire — not of the
model, so copying another provider's range in would assert something about
AIHubMix that no source states.
What the adapter does instead, unchanged from the last round: when a file already
carries
min/max, they are carried through rather than replaced with anunbounded control. On a create with no file, the bare budget is written as the
endpoint states it, because the alternative is inventing bounds. The 103 bare
budgets are reported upstream.
Intersecting a relay's options with its lab/peer baseline — declined
The suggestion was to narrow a relay's advertised tiers against what its lab
model or sibling routes advertise. That would put per-model knowledge back into
the adapter — the exact thing this rewrite removed (the
developer_id → labtable and the prefix/suffix strip list). The gateway's tier support is a property
of the gateway, not of the lab: AIHubMix deliberately accepts tiers the upstream
vendor does not name and maps them. An intersection would silently drop tiers
that demonstrably work. Where the endpoint is wrong, the fix belongs on the
endpoint; two such fixes have already shipped there during this PR.
Eight stale alias files removed
alicloud-glm-5.1,zai-glm-5.1,deep-deepseek-v4-pro,deep-deepseek-v4-flash,alicloud-deepseek-v4-pro,alicloud-deepseek-v4-flash,xiaomi-mimo-v2.5andxiaomi-mimo-v2.5-proaredeleted from
providers/aihubmix/models/.An earlier revision of this PR kept them and described them as "routing aliases
served but not listed". That was the wrong reading. The prefix on each is a
channel name —
alicloud,zai,deep,xiaomiare the upstream channelthe gateway routes through, not a distinct model — and each one answers HTTP 200
by relaying to a model already in this catalog, echoing that model's ID back in
the response. They duplicate
glm-5.1,deepseek-v4-pro,deepseek-v4-flashand
mimo-v2.5under a channel name."Callable" is therefore not the catalog boundary; the endpoint's main model list
is. Several hundred further routes are callable on the gateway without appearing
in it. The eight files predate that boundary being enforced.
A route can still rotate out of the list for a spell without being retired, so
deleteMissingstaysfalse— but the missing-file notice now names bothreadings so the next occurrence gets judged rather than kept by default.
What the interface-driven rewrite removed
The adapter originally landed 232 models, resolving relays to lab metadata with
a hand-maintained
developer_id → labtable plus a list of routing prefixes andsuffixes to strip. Both are gone. AIHubMix now serves
vendor,variant_ofand
open_weightson the endpoint itself, so nothing about a relay is inferredfrom its ID or mirrored in this repo any more — the adapter reads the catalog's
own answer. That change is what brings the catalog to 211.
base_modeldeveloper_identriesvendorfrom the endpointvariant_offrom the endpointopen_weightsHow models are matched to lab metadata
The endpoint answers both halves of the lookup itself:
vendornames the lab that built the model (292 of 408 routes).variant_ofnames the AIHubMix ID this entry is a routing variant of, andvariant_kindlabels it (pricing-tier26,channel-tier24,mode-preset18,deprecated-alias8).A relay is looked up under its own ID first, then under each
variant_ofhop,nearest first. Nearest-first matters:
qwen3.8-max-previewis declared avariant of
qwen3.8-maxand both are published lab models, so the relay factorsonto the preview it actually serves rather than onto the root of its chain.
Following a declared chain also reaches relays no string rule could have:
ox-alphazhipuai/glm-5.3-flashgrok-code-fast-1xai/grok-build-0.1cohere-command-acohere/command-a-03-2025grok-4-fast-reasoningxai/grok-4.3deprecated-alias: the old ID now routes to 4.3The one thing left in the adapter is
VENDOR_LABS, four entries for the labsthe two registries spell differently (
zhipu/zhipuai,moonshot/moonshotai,bytedance/bytedance-seed,meituan-longcat/meituan). It maps namespaces;no entry in it decides what a model is or which lab built it.
Lookups stay case-folded: AIHubMix lowercases every relay ID while labs keep
their own casing, so
minimax-m2has to reachminimax/MiniMax-M2.Dated release tags are deliberately left on.
gemini-2.5-pro-preview-06-05is apinned snapshot, not
google/gemini-2.5-pro— and the endpoint does not declareit a variant of one, which is now the actual test rather than a judgement call
in the adapter.
A resolved base means the entry inherits
open_weights,knowledge,licenseand the rest from
models/, and records only what actually differs — which iswhy 204 of 211 entries are three or four lines long.
Standalone entries are skipped, not guessed
A standalone entry is only legal where the response names no
vendor(above),and it still has to carry
release_date,open_weightsand both limits itself.The adapter skips anything that satisfies neither condition and reports it
through
skippedNoticeand a deduped issue rather than inventing a value.open_weightsused to block every standalone create because the endpoint didnot serve it. It now does, for 289 of 408 routes, which is what lets the adapter
create standalone entries at all. The guard also checks limits now: the endpoint
sends
max_output: 0for a ceiling it does not know, and without that checkthose models reach the writer and fail
ModelBasevalidation.What is still uncovered, entirely on the endpoint side:
vendor, so they cannot be factoredrelease_dateopen_weightsmax_output: 0Every one of these shrinks as AIHubMix backfills its own catalog. The adapter
needs no change for any of them.
Sentinel values in the source data
Two signals mean "unknown", not "zero" or "huge", and are read as absent so the
field falls through to the base model:
max_output: 0max_output >= context_lengthNo value is invented — the field is simply omitted and inherited.
Reasoning options
The endpoint carries an extra
defaultkey insidereasoning_options[]thatReasoningOptiondoes not accept, and two non-standard effort spellings,no_thinkandinstant. The adapter drops the unknown key and maps thespellings onto
none/minimal, filtering anything else outsideReasoningEffortValue, and folds a toggle into a graded effort list that alreadycarries
none. These are shape-level normalisations — vocabulary and optionshape, not per-model judgement. All four source quirks are reported upstream to
AIHubMix.
Deletion policy
deleteMissingisfalseandtrackMissingModelsistrue. AIHubMix rotatesroutes in and out; a transient absence should not delete a catalog entry. The
sourceIDhook returnsundefinedforretire_stage === "deprecated"soretired routes are marked, not removed.
Three framework changes
formatTomlnow emitsinput_audio/output_audioinside cost tiers. Bothare already in
Cost, but were silently dropped when writing tiered pricing.translateModel's context gains an optionalheader(id)accessor, so aprovider that owns its header can refresh the block it generates without
discarding the notes a human wrote around it. Optional, so no other provider
changes.
SyncProvidergainstrackMissingModels?: boolean, so a provider that createsmodels but still skips the ones it cannot write opens issues for those skips.
Implied by
skipCreates; the two providers that would be affected already setit. The issue body wording is corrected alongside it — it asserted the provider
used
skipCreates, which is no longer true of every caller.Verification
bun run validate— exit 0, aihubmix 211 models in the built catalogbun test packages/core/test/sync.test.ts— 219 pass, 2 fail (bothpre-existing on
dev: DeepInfra modalities and an LLM Gateway case-variantassertion that only fails on case-insensitive filesystems). Three new tests
cover the limit-restatement rule, the standalone refusal, and the
create-but-skip issue path.
bunx tsc --noEmit -p packages/core— unchanged from thedevbaselinedevbaseline —142 created, 69 updated, 0 removed;running it again —
0 created, 0 updated, 0 removed(idempotent)dev— no provider other than aihubmix changesNote for maintainers on the sync gate
aihubmix is not in
REVIEWED_REASONING_PROVIDERS, and most of its routes arereasoning models, so the hourly sync PR will be classified unsafe on nearly
every run and wait for a human. That is the right default for a new adapter —
happy to leave it there until it has a track record.
🤖 Generated with Claude Code