Add AIHubMix sync provider - #7042
Open
0genlab wants to merge 22 commits into
Open
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>
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>
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>
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>
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>
…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>
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>
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>
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>
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>
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>
…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>
Contributor
Action items
|
… deprecations Three findings from the PR review, two of them real. Empty reasoning_options on creates. A route the endpoint reports as reasoning while publishing no reasoning_options reached the runner with the field unset, and the runner reads that on a reasoner as "no caller control" and stamps `[]` — which AGENTS.md forbids using for uncertainty. The guard meant to stop it compares against the base model's options, and `ModelMetadata` has no `reasoning_options` field, so that comparison is always undefined and the guard never fires. The adapter now throws MissingReasoningOptionsError on the two paths that actually write, the same as the Cloudflare adapter: 13 creates in the current listing are skipped and surface as missing models instead of being published with an invented "no controls". An authored `[]` is a human statement and is carried through untouched. Stale deprecations. `status` only ever wrote `deprecated` and otherwise handed the file's value back, so a route that came back from retirement kept the mark for good. `retire_stage` is present on every route (407 active, 2 deprecated), so it is authoritative about retirement — and only about retirement: a non-deprecated stage now clears a `deprecated` status and leaves `alpha`/`beta` alone. Bare budget_tokens was also raised and is not a defect: the endpoint publishes 103 of them itself, AGENTS.md's own example authors the same bare shape with the wire path in the header, and 256 of the 354 budget entries in the repo are bare. There is no bounded baseline being shadowed — `ModelMetadata` cannot state a budget range, and a peer provider's bounds describe that host's API, not this one's. Also exports REASONING_EFFORT_VALUES from the schema so the effort filter reads the catalog's own list rather than restating it. Verified: 222 pass / 2 fail (both pre-existing on dev), tsc unchanged at 164 lines, validate exit 0, sync 129 created / 69 updated with a second run at 0/0/0, and no file newly carrying `reasoning_options = []`. Each fix was reverted in turn and a test failed for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reviewer raised both again this round. Both answers already lived in the PR description, which the reviewer reads, but the reasoning belongs in `sync.md` where it survives the PR: the bare budget shape is what AGENTS.md itself authors and what 256 of the 354 budget entries in `providers/` use, and a dropped alias is one the list omits while the ID it routes to is on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
Pushed Fixed
Answered, no change
222 pass / 2 fail (both pre-existing on |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 22 files — 5 source, 8 note moves, 8 alias deletions, and
sync.md.Answers to the review findings
togglealongside an effort list containingnoneis intentional, not a bugThe reviewer flagged models such as
glm-5.2that publish both{type = "toggle"}and{type = "effort", values = [..., "none", ...]}as"contradictory metadata" that the adapter should reconcile. It is neither
contradictory nor a defect in the source — it is the gateway's design, and this
repo already documents it.
AIHubMix is a multi-dialect gateway: the same route is reachable over the
OpenAI-compatible
/v1/chat/completionspath, the Anthropic-compatible/v1/messagespath, and the Gemini native path. Each dialect has its ownoff-switch, and the gateway maps whichever one the caller sends onto the
vendor's real control instead of rejecting it.
providers/aihubmix/provider.tomlon
devalready spells all four out:$.reasoning_effort = "none"|"minimal"|…|"xhigh";"none"disables$.thinking.type = "enabled"|"disabled"|"adaptive"$.generationConfig.thinkingConfig,thinkingBudget: 0off$.reasoning.effortSo one model genuinely has two independent ways to turn thinking off, which is
exactly
toggle+ an effort list containingnone. Verified live againstglm-5.2on 2026-09-11 with a fixed prompt:reasoning_effort: "none"enable_thinking: falsereasoning_effort: "minimal"reasoning_effort: "max"reasoning_effort: "turbo"(bogus)That last row is the point: an unrecognised effort is mapped to the nearest
supported tier rather than erroring, because callers arrive from many SDKs with
many vocabularies. Collapsing the two signals in the adapter would delete a real,
independently reachable control from the catalog.
(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
behaviour. Corrected publicly in a later comment.)
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 toggle — the same shape the OpenRouter adapter uses — so the wire
path survives every rewrite instead of depending on a human re-adding it.
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.
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 308.
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 273 of 308 entries are three or four lines long.
Standalone entries are skipped, not guessed
A full catalog entry has to carry
release_date,open_weightsand both limitsitself. The adapter skips anything that has neither lab metadata to inherit
from nor those fields, and reports it through
skippedNoticerather thaninventing 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. These are shape-level normalisations — vocabularymapping, not per-model judgement — and all four 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.
Framework changes
Four, all small and all needed by something above:
formatTomlnow emitsinput_audio/output_audioinside cost tiers. Bothare already in
Cost, but were silently dropped when writing tiered pricing.translateModel's options gainheader(id), the leading comment block alreadyon the file.
authoritativeHeadersreplaces that block wholesale, so withoutreading it first a provider that owns its header cannot refresh the part it
generates without discarding the notes a human wrote around it.
trackMissingModelsis now settable on its own. It was only ever readalongside
skipCreates, so a provider that creates most models but skips theones it cannot write produced notices nobody acts on. The missing-model issue
text is reworded to cover that second reading.
normalizeModelSlugis exported fromopenrouter.ts, which already serves asthe shared helper module for the other provider adapters. Two more adapters
(
venice.ts,wandb.ts) hold byte-identical private copies; left alone here.Limits are resolved against the lab window, not just accepted
Three rules, in this order. Each was written because the previous revision got a
real route wrong.
A restatement is a ratio, not a direction. The endpoint quotes an accepted
204800 as 200000 and an accepted 1000000 as 1048576. Neither is the host stating
a different window. A decimal restatement of a binary window loses at most
1000/1024per K unit, so1000³/1024³— three nested unit swaps — is the floorof what a restatement can explain, and two limits within that floor of each other
are one window spelled twice. Checking only the narrowing side left 20 routes
writing an override that states no difference at all (
glm-5.3recording 1048576against a lab window of 1000000).
The lab entry is the spelling a restatement resolves to, and the provider
file's own value only where the lab states no such key. Matching the lab lets
factoring drop the key entirely; resolving to whatever the file happens to hold
would pin that spelling forever —
qwen3.7-flashcarries 991000, which is onlyan imprecise way of writing the lab's 1000000. Resolving file-first preserved 10
such overrides,
claude-opus-4-8among them still recording 200000/32000 againsta 1000000/128000 lab window. That one is the finding from review: the endpoint
agrees with the lab there, and the narrowing number was the file's.
Then clamp to the lab's window — a relay cannot serve a wider one than the
model it relays; the window is the model's property and a host can only restrict
it. The clamp runs after the resolution rather than instead of it, because an
endpoint quoting back the same stale ceiling the file already holds resolves to
that number and only a later clamp catches it.
grok-4.5is exactly this: thefile held 1000000 for both limits against a 500000 lab window, and the endpoint
quotes that same 1000000.
Past the two
max_outputsentinels, 14 routes quote a window wider than theirlab entry's (
qwen3.8-2.4t-a95bat 1000000 of 262144,gemma-4-31b-itat 131100of 32768). All are reported upstream to AIHubMix; where the lab entry is the
stale side,
models/is where that gets corrected. After all three rules, thefactored files write 7 limit keys total — every one a genuine host cap, none
byte-identical to its lab entry.
Headers keep human notes and never outlive their block
authoritativeHeadersreplaces the leading block on every sync, so what countsas "the block" has to be exact in both directions.
with (
# Toggle:,# Effort:,# Budget:,# Off is effort— the openingsAGENTS.mdprescribes), not by whether it mentions a field path or the docshost. Keying on the substring would also delete lines that merely contain one:
two files state a wire path together with a dated live test the response cannot
reproduce. A second statement of the same path costs nothing; a deleted
verification date cannot be recovered. Matched on the trimmed line, or an
indented
# Toggle:outlives the block it documented.restatement of the block, so they go whether or not a block replaces them. Only
the docs link survives as a note, and only where no derived block restates it.
Keeping the wire paths when nothing was derived is what left a route
advertising a toggle it no longer had — the block vanished, its tail survived
as a "note", and no later sync could tell the difference, so the file never
self-corrected.
AGENTS.mdrequires these notes above the first key, since a sync keeps onlythe leading block and drops every comment below it. Eight files in this
provider carried theirs mid-body; they are moved up here, which is 8 of the 22
files in this diff. Without the move the next sync deletes them silently.
Display names
The endpoint's label is recorded only where the relay is not that lab model under
other punctuation — its bare ID, normalised, differs from the base model's
slug. Bare, because that is what resolved the base model in the first place
(
Qwen/QwQ-32Breachesqwen/qwq-32b); normalising the namespaced form matchesnothing, and each of the 10 namespaced routes would take a redundant storefront
override the moment its lab file lands.
This keeps
coding-glm-4.6-freereading "Coding GLM 4.6 (free)" rather than thebare "GLM-4.6" it would share with two other routes, while entries differing only
in punctuation (
GLM 5.3against the lab'sGLM-5.3) defer to the lab and writenothing. A name already on the file outranks both — four files spell their model
the way its lab does (
MiMo-V2.5) where the endpoint sends a storefrontMimo V2.5— and handing it through stays correct because factoring drops a namethe lab states identically.
A blank label is not a name.
ModelBase.nameismin(1), so writing one throughaborts the entire provider's sync at validation rather than skipping the
field. 0 of the 409 live routes send one today, which is exactly why it needed a
test rather than a live check.
Verification
bun run validate— exit 0bun test packages/core/test/sync.test.ts— 220 pass, 2 fail (bothpre-existing on
dev: DeepInfra modalities and an LLM Gateway case-variantassertion that only fails on case-insensitive filesystems)
reverted, checked one at a time
bunx tsc --noEmit -p packages/core— no aihubmix errors129 created, 69 updatedthen0 created, 0 updated, 0 removedwith 198 unchanged (idempotent)dev— no provider other than aihubmix changesReview round: two fixed, two answered
Empty
reasoning_optionson creates — fixedA route the endpoint reports as reasoning while publishing no
reasoning_optionsreached the runner with the field unset, and the runnerreads that on a reasoner as "no caller control" and stamps
[]— whichAGENTS.mdrule 4 forbids using for uncertainty. The guard meant to prevent itcompares against the base model's options, and
ModelMetadataBasehas noreasoning_optionsfield, so that comparison is alwaysundefinedand theguard never fires.
The adapter now throws
MissingReasoningOptionsErroron the two paths thatactually write, the same as the Cloudflare adapter. 13 creates in the
current listing are skipped and surface as missing models instead of being
published with an invented "no controls". An authored
[]already on a file isa human stating the host exposes no control, and is carried through untouched
(
grok-build-0.1ondev).Stale
deprecatednever cleared — fixedstatusonly ever wrotedeprecatedand otherwise handed the file's valueback, so a route that came back from retirement kept the mark for good.
retire_stagerides on every route (407 active, 2 deprecated), so it isauthoritative about retirement — and only about retirement. A non-deprecated
stage now clears a
deprecatedstatus;alphaandbetaare left alonebecause the endpoint says nothing about either.
Bare
budget_tokens— not a defectRaised as "inventing unbounded controls that shadow real bounds". Three
reasons it is neither:
budget_tokensentries itself. The adapter relays a declaration; it does not synthesise one.
ModelMetadatahas noreasoning_optionsfield, soa lab entry cannot carry a budget range for a provider file to override. A
first-party entry for the same model states that host's API contract, which
does not transfer — AIHubMix's Anthropic-compatible path rejects a manual
budget_tokenson the Opus tier, which the affected files record in theirheaders, so copying
min = 1024across would be wrong.AGENTS.md's Qwen3.5 Plus example authors{ type = "budget_tokens" }bare with the wire path in the header comment,and 256 of the 354 budget entries under
providers/are bare(
requesty138,alibaba23,neuralwatt16). Authoredmin/maxon the8 aihubmix files that have them are preserved, not replaced.
Copying bounds from a peer provider would also put per-model knowledge back
into the adapter, which is what this rewrite exists to remove.
Deleted channel aliases — no gap
Flagged as deletions into a catalog gap. Each of the 8 dropped alias files is
absent from the model list while the ID it routes to is on it, so every
one is replaced by an entry the first sync writes. The gap is visible only
because this PR ships the adapter without its generated files.
Note 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