Skip to content

Add AIHubMix sync provider - #7042

Open
0genlab wants to merge 22 commits into
anomalyco:devfrom
0genlab:feat/aihubmix-sync
Open

Add AIHubMix sync provider#7042
0genlab wants to merge 22 commits into
anomalyco:devfrom
0genlab:feat/aihubmix-sync

Conversation

@0genlab

@0genlab 0genlab commented Sep 14, 2026

Copy link
Copy Markdown

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.

GET https://aihubmix.com/api/v1/models?type=llm

This PR ships the adapter only

An earlier revision carried the 299 generated providers/aihubmix/models/*.toml
files alongside the adapter. That pushed the diff past GitHub's 300-file ceiling,
gh pr diff --patch answered HTTP 406, and pr-reviewer.yml died in its
context 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.yml regenerates them on automation/sync-models-aihubmix, a branch
the 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

toggle alongside an effort list containing none is intentional, not a bug

The reviewer flagged models such as glm-5.2 that 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/completions path, the Anthropic-compatible
/v1/messages path, and the Gemini native path. Each dialect has its own
off-switch, and the gateway maps whichever one the caller sends onto the
vendor's real control instead of rejecting it. providers/aihubmix/provider.toml
on dev already spells all four out:

  • Raw Chat — $.reasoning_effort = "none"|"minimal"|…|"xhigh"; "none" disables
  • Raw Messages — $.thinking.type = "enabled"|"disabled"|"adaptive"
  • Raw Gemini — $.generationConfig.thinkingConfig, thinkingBudget: 0 off
  • Raw Responses — $.reasoning.effort

So one model genuinely has two independent ways to turn thinking off, which is
exactly toggle + an effort list containing none. Verified live against
glm-5.2 on 2026-09-11 with a fixed prompt:

request reasoning chars result
reasoning_effort: "none" 0 off
enable_thinking: false 0 off
both together 0 coherent, no error
reasoning_effort: "minimal" 217 tier live
reasoning_effort: "max" 328 tier live
reasoning_effort: "turbo" (bogus) 302 HTTP 200, mapped — not 400

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 caller
which field to send, and that a sync rewrites the file whole and would erase a
hand-written header. translateModel now emits the header itself whenever the
model 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 → lab
table 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 → lab table plus a list of routing prefixes and
suffixes to strip. Both are gone. AIHubMix now serves vendor, variant_of
and open_weights on the endpoint itself, so nothing about a relay is inferred
from 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.

first revision now
aihubmix models in the built catalog 232 308
entries using base_model 217 273
standalone full definitions 15 35
lab mapping 21 hand-maintained developer_id entries vendor from the endpoint
relay → base matching 16 prefixes + 10 suffixes stripped from the ID variant_of from the endpoint
open_weights not served; every standalone create blocked served for 289 of 408

How models are matched to lab metadata

The endpoint answers both halves of the lookup itself:

  • vendor names the lab that built the model (292 of 408 routes).
  • variant_of names the AIHubMix ID this entry is a routing variant of, and
    variant_kind labels it (pricing-tier 26, channel-tier 24,
    mode-preset 18, deprecated-alias 8).

A relay is looked up under its own ID first, then under each variant_of hop,
nearest first. Nearest-first matters: qwen3.8-max-preview is declared a
variant of qwen3.8-max and both are published lab models, so the relay factors
onto 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:

relay resolves to why a prefix/suffix rule can't
ox-alpha zhipuai/glm-5.3-flash codename shares no substring with the model
grok-code-fast-1 xai/grok-build-0.1 renamed upstream
cohere-command-a cohere/command-a-03-2025 the date tag is the lab's, not the relay's
grok-4-fast-reasoning xai/grok-4.3 deprecated-alias: the old ID now routes to 4.3

The one thing left in the adapter is VENDOR_LABS, four entries for the labs
the 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-m2 has to reach minimax/MiniMax-M2.

Dated release tags are deliberately left on. gemini-2.5-pro-preview-06-05 is a
pinned snapshot, not google/gemini-2.5-pro — and the endpoint does not declare
it 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, license
and the rest from models/, and records only what actually differs — which is
why 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_weights and both limits
itself. The adapter skips anything that has neither lab metadata to inherit
from nor those fields, and reports it through skippedNotice rather than
inventing a value.

open_weights used to block every standalone create because the endpoint did
not 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: 0 for a ceiling it does not know, and without that check
those models reach the writer and fail ModelBase validation.

What is still uncovered, entirely on the endpoint side:

  • 116 routes carry no vendor, so they cannot be factored
  • 104 date to no release_date
  • 119 state no open_weights
  • 104 quote max_output: 0

Every 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:

signal count why
max_output: 0 104 a servable model cannot emit zero tokens
max_output >= context_length 36 the context window quoted a second time, leaving no room for the prompt

No value is invented — the field is simply omitted and inherited.

Reasoning options

The endpoint carries an extra default key inside reasoning_options[] that
ReasoningOption does not accept, and two non-standard effort spellings,
no_think and instant. The adapter drops the unknown key and maps the
spellings onto none / minimal, filtering anything else outside
ReasoningEffortValue. These are shape-level normalisations — vocabulary
mapping, not per-model judgement — and all four are reported upstream to
AIHubMix.

Deletion policy

deleteMissing is false and trackMissingModels is true. AIHubMix rotates
routes in and out; a transient absence should not delete a catalog entry. The
sourceID hook returns undefined for retire_stage === "deprecated" so
retired routes are marked, not removed.

Framework changes

Four, all small and all needed by something above:

  • formatToml now emits input_audio / output_audio inside cost tiers. Both
    are already in Cost, but were silently dropped when writing tiered pricing.
  • translateModel's options gain header(id), the leading comment block already
    on the file. authoritativeHeaders replaces that block wholesale, so without
    reading it first a provider that owns its header cannot refresh the part it
    generates without discarding the notes a human wrote around it.
  • trackMissingModels is now settable on its own. It was only ever read
    alongside skipCreates, so a provider that creates most models but skips the
    ones it cannot write produced notices nobody acts on. The missing-model issue
    text is reworded to cover that second reading.
  • normalizeModelSlug is exported from openrouter.ts, which already serves as
    the 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/1024 per K unit, so 1000³/1024³ — three nested unit swaps — is the floor
of 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.3 recording 1048576
against 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-flash carries 991000, which is only
an imprecise way of writing the lab's 1000000. Resolving file-first preserved 10
such overrides, claude-opus-4-8 among them still recording 200000/32000 against
a 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.5 is exactly this: the
file held 1000000 for both limits against a 500000 lab window, and the endpoint
quotes that same 1000000.

Past the two max_output sentinels, 14 routes quote a window wider than their
lab entry's (qwen3.8-2.4t-a95b at 1000000 of 262144, gemma-4-31b-it at 131100
of 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, the
factored 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

authoritativeHeaders replaces the leading block on every sync, so what counts
as "the block" has to be exact in both directions.

  • A superseded wire path is told from a note to keep by what the line opens
    with
    (# Toggle:, # Effort:, # Budget:, # Off is effort — the openings
    AGENTS.md prescribes), not by whether it mentions a field path or the docs
    host. 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.
  • The two lines naming this gateway's wire paths are the adapter's own
    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.md requires these notes above the first key, since a sync keeps only
    the 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-32B reaches qwen/qwq-32b); normalising the namespaced form matches
nothing, 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-free reading "Coding GLM 4.6 (free)" rather than the
bare "GLM-4.6" it would share with two other routes, while entries differing only
in punctuation (GLM 5.3 against the lab's GLM-5.3) defer to the lab and write
nothing. 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 storefront
Mimo V2.5 — and handing it through stays correct because factoring drops a name
the lab states identically.

A blank label is not a name. ModelBase.name is min(1), so writing one through
aborts 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 0
  • bun test packages/core/test/sync.test.ts — 220 pass, 2 fail (both
    pre-existing on dev: DeepInfra modalities and an LLM Gateway case-variant
    assertion that only fails on case-insensitive filesystems)
  • every rule above is pinned by a test that fails when the rule alone is
    reverted, checked one at a time
  • bunx tsc --noEmit -p packages/core — no aihubmix errors
  • two consecutive real syncs — 129 created, 69 updated then
    0 created, 0 updated, 0 removed with 198 unchanged (idempotent)
  • all 8 moved notes survive a real sync; 0 orphaned wire-path lines left behind
  • built-catalog diff against dev — no provider other than aihubmix changes

Review round: two fixed, two answered

Empty reasoning_options on creates — fixed

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 rule 4 forbids using for uncertainty. The guard meant to prevent it
compares against the base model's options, and ModelMetadataBase 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 [] already on a file is
a human stating the host exposes no control, and is carried through untouched
(grok-build-0.1 on dev).

Stale deprecated never cleared — fixed

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 rides 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; alpha and beta are left alone
because the endpoint says nothing about either.

Bare budget_tokens — not a defect

Raised as "inventing unbounded controls that shadow real bounds". Three
reasons it is neither:

  • Not invented. The endpoint publishes 103 bare budget_tokens
    entries itself. The adapter relays a declaration; it does not synthesise one.
  • Nothing is shadowed. ModelMetadata has no reasoning_options field, so
    a 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_tokens on the Opus tier, which the affected files record in their
    headers, so copying min = 1024 across would be wrong.
  • It is the repo's own shape. 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
    (requesty 138, alibaba 23, neuralwatt 16). Authored min/max on the
    8 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 are
reasoning 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

chenxue and others added 20 commits September 9, 2026 11:14
`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>
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>
@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/aihubmix.ts / packages/core/src/sync/index.ts (preserveReasoningOptions) - Check: reasoning = true ⇒ real host reasoning_options; relays must not use [] from uncertainty (AGENTS.md Reasoning options). Why: On creates, when the endpoint omits reasoning/reasoning_options (PR notes ~107 routes omit reasoning), buildAihubmixModel leaves both unset, then the runner resolves lab reasoning = true and preserveReasoningOptions stamps reasoning_options = []. Lab metadata has no reasoning_options, so that empty array is written on the provider file and means “no caller control,” wiping GPT/Claude/DeepSeek-style controls for every such create. Action: On create, if the resolved model reasons and the endpoint did not yield usable options, skip the model (undefined + missing-model issue) or throw MissingReasoningOptionsError—do not emit []. Only write [] when the host affirmatively has no control.
  • [high] [possible mistake] providers/aihubmix/models/xiaomi-mimo-v2.5.toml / xiaomi-mimo-v2.5-pro.toml (deletions) - Check: Catalog boundary is the main listed ID; do not drop the only local route for a served model. Why: Patch 19 deletes these as “hidden channel aliases,” but this tree has no mimo-v2.5.toml / mimo-v2.5-pro.toml (or other primary stand-ins). Peer providers catalog mimo-v2.5 under that id; aihubmix only had the xiaomi-mimo-v2.5* files. Until automation recreates a listed id, MiMo V2.5 disappears from aihubmix. Same risk for alicloud-/deep-/zai- DeepSeek/GLM deletes while bare deepseek-v4-pro / glm-5.1 are also absent. Action: Confirm each deleted id is unlisted and only aliases a listed id that this PR or the first sync will author. If xiaomi-mimo-v2.5 (etc.) is the listed id, keep it (or add the real listed id here) instead of deleting into a gap.
  • [medium] [possible mistake] packages/core/src/sync/providers/aihubmix.ts (status / retire_stage) - Check: Deprecation should follow the endpoint when it is authoritative for status. Why: status is set to "deprecated" when retire_stage === "deprecated", otherwise existing?.status is kept. A once-deprecated relay that leaves retirement stays deprecated forever (unlike DigitalOcean/DeepInfra, which clear stale deprecation). Action: When retire_stage is present and not deprecated, clear a prior "deprecated" status (keep unrelated alpha/beta if you still preserve those from the file).
  • [medium] [violation] packages/core/src/sync/providers/aihubmix.ts (reasoningOptions / budget_tokens) - Check: budget_tokens bounds only when verified; bare budget must not pretend to be a full control surface when lab/peer hosts document ranges. Why: Creates write endpoint-bare { type = "budget_tokens" } with no min/max. Authored bounds are only merged on update. First-time Claude/Gemini-style creates therefore publish unbounded budget where peers use real ranges (e.g. min 1024), and nothing re-applies bounds until a human edits the file. Action: For creates with bare budget_tokens and no authored range, either omit budget until bounds are known, copy a verified same-host peer range, or skip/issue the model—do not ship unbounded budget as if it were complete host metadata.
  • [low] [possible mistake] PR body / first post-merge sync - Check: Data-changing automation should be reviewable against citations. Why: This PR ships the adapter and deletes routes but defers ~300 generated TOMLs to sync-models-aihubmix, which the reviewer workflow skips—so the bulk of pricing/limits/options never gets the same review. Action: After the first automation run, spot-check a sample of new/updated files (reasoning shapes, costs, primary MiMo/DeepSeek/GLM ids) against the public catalog and lab peers, or temporarily include a representative generated subset in a follow-up PR.

chenxue and others added 2 commits September 14, 2026 14:21
… 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>
@0genlab

0genlab commented Sep 14, 2026

Copy link
Copy Markdown
Author

Pushed ef0f2c2ec + 5c0590c84 for this round. Two of the four findings were real; the other two are answered in the description (the reviewer reads the PR body, not comments, so that is where the detail lives).

Fixed

  • Empty reasoning_options on creates. Real, and the root cause is a guard that can never fire: preserveReasoningOptions compares against the base model's options, but ModelMetadataBase has no reasoning_options field, so the comparison is always undefined. 14 files were getting [] stamped on models whose lab entry says reasoning = true. The adapter now throws MissingReasoningOptionsError on the two paths that write — same as cloudflare-ai-gateway.ts — so 13 creates are skipped and surface as missing models instead. An authored [] is a human statement and is left alone.
  • Stale deprecated never cleared. Real. retire_stage is present on every route (407 active, 2 deprecated), so a non-deprecated stage now clears a deprecated status while leaving alpha/beta untouched, following digitalocean.ts:409-411.

Answered, no change

  • Bare budget_tokens is not invented: the endpoint publishes 103 of them, AGENTS.md's own Qwen3.5 Plus example authors the same bare shape with the wire path in the header, and 256 of the 354 budget entries under providers/ are bare. Nothing is shadowed — ModelMetadata cannot state a budget range, and a first-party entry's bounds describe that host's API (AIHubMix's Anthropic-compatible path rejects a manual budget_tokens on the Opus tier, which those files record in their headers).
  • Dropped channel aliases leave no gap: each of the 8 is absent from the model list while the ID it routes to is on it, so each is replaced by an entry the first sync writes. The gap only looks real because this PR ships the adapter without its generated files.

222 pass / 2 fail (both pre-existing on dev), tsc unchanged, validate exit 0, sync 129 created, 69 updated then 0/0/0, and no file newly carries reasoning_options = []. Each fix was reverted in turn and a test failed for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant