feat(sync): add kenari sync adapter - #3171
Conversation
Action items
|
|
Addressed the action item: the PR body now states that effort values are sourced verbatim from the endpoint's |
|
No actionable findings. |
|
Opened #3380 as a data-only refresh of the kenari catalog, generated by running this adapter against the current If #3380 lands first I will rebase this PR so its first sync becomes a no-op and it reduces to the adapter code plus the idempotence proof. Happy to also fit it into the labeling scheme from #3370 ( |
Adds 18 models and removes 3 no longer served, generated by running the kenari sync adapter (PR #3171) against the current dev branch. Cost stays 0 by policy (IDR prepaid wallet), reasoning_options come verbatim from the endpoint.
Adds 18 models and removes 3 no longer served, generated by running the kenari sync adapter (PR anomalyco#3171) against the current dev branch. Cost stays 0 by policy (IDR prepaid wallet), reasoning_options come verbatim from the endpoint.
Data-only sync against https://kenari.id/v1/models. Cost stays 0 per the IDR-wallet billing policy. Added (10): claude-opus-5, claude-sonnet-4-6, gemini-3-6-flash, minimax-m2-7, minimax-m2-7-highspeed, nemotron-3-ultra-550b-a55b:free, qwen3-7-flash, qwen3-8-max, step-3-7-flash, step-3-7-flash:free. Removed (3): grok-build-0-1, kimi-k2-6:free, kimi-k2-7-code:free. Skipped (4): laguna-s-2-1:free, longcat-2-0:free, mimo-v2-5-pro-ultraspeed, north-mini-code:free. No canonical models/ metadata exists for these IDs yet; hand-author when an upstream entry lands. The kenari sync adapter (anomalyco#3171) was used to generate this catalog locally and will rebase onto this branch separately.
Rebases anomalyco#3171 onto current upstream/dev (458519b). Original data-file changes dropped: they were 30 days stale and the catalog refresh lives in chore/kenari-catalog-refresh-aug, which this PR is meant to follow. Adapter code only: - Public /v1/models endpoint, no API key or repo secret needed. - Cost stays 0 by policy: Kenari bills in IDR from a wallet at its own catalog prices, so USD figures would be a misleading conversion. - Existing factored TOMLs are preserved as authored; limits keep inheriting the canonical base model (Kenari's context figures are themselves ingested from models.dev, so they are not authoritative). - New models sync only when a canonical models/ metadata entry exists (dash IDs resolve to dotted canon, e.g. grok-4-5 -> xai/grok-4.5); unresolvable models are skipped with a sync-report notice. Idempotence proof: with this commit on top of chore/kenari-catalog-refresh-aug, bun models:sync kenari produces 0 created / 0 updated / 0 removed / 45 unchanged across two consecutive runs. bun validate exits 0. Structural adaptation from the original PR: index.ts gained several new providers (hyper, merge-gateway, nano-gpt, ofox, cortecs, requesty, tinfoil, baseten) and aggregators group became a multi-line array with merge-gateway/nano-gpt/ofox/requesty slots. The kenari import + provider registration + aggregators entry were re-inserted alphabetically.
8a18661 to
d0168e9
Compare
|
Rebased onto current I opened #4393 as the data-only refresh, generated by running this adapter on current The idempotence is measured, not assumed: running the adapter twice against the refreshed data gives One thing worth flagging rather than hiding: the live endpoint lists 50 models and the synced set is 45. The 5 without an entry have no canonical Also still happy to take the |
Action items
|
…no controls An omitted list meant the adapter emitted [], which is an affirmative no-control assertion. Because [] is not nullish it defeated the ?? existing.reasoning_options fallback, so a hand-authored effort list was wiped whenever the endpoint left efforts unset. Only an explicitly empty list from the API carries that meaning now. Adds three regression tests: preservation on omission, an explicit empty list, and a published list.
|
All four review items are fixed, each with a test that fails when the fix is reverted.
Also dropped 9 tests added. Full The base-resolution fix changed the data, so I regenerated #4393: it now covers 48 of the 50 live models instead of 45. Happy to take the |
Action items
|
…from the source file Two follow-ups from review. resolveKenariBaseModel only tried the canonical provider prefix, so a model whose owned_by is a lab name we do not alias was skipped even when models/<lab>/<id>.toml existed. It now falls back to resolving the bare ID against the metadata filenames, which strips :free and takes only an unambiguous single match, so a wrong guess stays a skip. canonicalName read models/ relative to the working directory, so :free display names silently lost their (Free) suffix whenever the sync ran from anywhere but the repo root. The root now comes from import.meta.dirname, matching openrouter and requesty. Adds three more tests: dotted-ID resolution, resolution with an unusable owned_by, and :free naming with zero-cost factored output.
…to what is read Parsing reasoning_options as an enum meant one unrecognised effort string threw in parseModels and failed the whole hourly sync instead of the single field. Efforts are parsed as strings now and filtered to the catalog set, deduped. A list whose values are all unknown is treated as silence rather than as an assertion that the control is gone. Also drops modalities and tool_call from the schema, which were declared but never read (capability facts come from the canonical base, and passthrough keeps the raw payload), and passes limit straight to factorBaseModel instead of an all-undefined object that resolved to the same thing. Adds three tests: an unknown effort parses, unknown values are dropped while known ones survive, and an all-unknown list keeps the authored efforts.
|
Checking one more interaction: whether the create path and |
A canonical filename can carry a version the host's ID drops. Kenari serves
`north-mini-code:free` while the lab entry is
`models/cohere/north-mini-code-1-0.toml`. Every resolution path compares
filenames exactly (`resolveCanonicalBaseModel` builds `${candidate}.toml`,
`resolveModelMetadataBaseModel` falls back to an exact ID then an exact
basename), and `canonicalCandidates` rewrites only `-fast` plus a few
lab-specific shapes, so nothing bridges that gap.
The model was therefore skipped as "no canonical metadata entry" while its
metadata sat in the tree the whole time. That is the failure mode this adapter
exists to remove: the skip is silent, it reads as missing lab data, and the
answer looks like a hand-authored PR per model.
Adds a last-resort pass that matches a bare ID against canonical entries whose
name is that ID plus a trailing release marker, and takes it ONLY when exactly
one exists anywhere in the tree. Stems with siblings (`mistral-large`, `gpt-4o`,
`claude-opus`) stay unresolved, because choosing a release on the host's behalf
is a guess and a wrong `base_model` is worse than a visible skip. The marker
pattern is digits only, so a word suffix like `-code` or `-flash` can never
collapse two different models. The index is built once per process.
This runs after every existing path, so no ID that resolves today changes.
nano-gpt solves the same case with a hand-maintained BASE_MODEL_ALIASES entry
(`cohere/north-mini-code`). That works, but it needs an edit per model forever,
which is the opposite of what an hourly unattended sync is for. Happy to switch
to an alias table if you would rather keep the mechanism uniform across
adapters.
Two tests, both break-tested: the north-mini case was red before the fix, and
relaxing the single-match guard to `>= 1` turns the ambiguity test red, so the
guard is doing real work rather than passing by luck.
`bun test` is unchanged at 4 pre-existing failures (open-weight metadata
weights links, DeepInfra modalities, and two snapshot tests), verified against a
clean checkout of this branch before and after.
|
Checking how resolved existing models expose |
Adds Kenari (kenari.id) to the hourly model sync workflow so its catalog stays current automatically instead of via hand-opened PRs.
How it works
https://kenari.id/v1/modelsendpoint. No API key and no repo secret needed.providers/kenarientry.models/metadata entry exists to inherit capability facts from (factorBaseModel). Kenari's dash IDs resolve to the dotted canon generically (grok-4-5->xai/grok-4.5), no per-model alias table.reasoning_optionsarray (per-model, e.g."reasoning_options":["low","medium","high","xhigh","max"]onclaude-fable-5). A model with"reasoning": trueand no advertised efforts getsreasoning_options = [], matching the existing hand-authored entries.Scope
This PR is the adapter only:
packages/core/src/sync/index.ts,packages/core/src/sync/providers/kenari.ts, and its test. No TOMLs are added, changed, or deleted here. An earlier revision of this description carried a "first sync (included in this PR)" table, which was a dry-run result rather than a file list. Removed, since nothing in the diff writes a model file.Where the catalog stands today
Measured 2026-08-10 against the live endpoint and this repo's default branch:
providers/kenari/models/*.tomlin this repogrok-build-0-1,kimi-k2-6:free,kimi-k2-7-code:free)The 15 is an upper bound on creations, not a prediction: any of them without a canonical
models/entry is skipped and reported instead. Most of that drift is also covered by #4393, so whichever of the two lands first shrinks the other's first run.Reasoning effort values advertised by the endpoint
Re-checked against live on 2026-08-10, all still current:
/v1/modelsreasoning_options["low","medium","high","xhigh","max"]["none","low","medium","high","xhigh","max"]["low","medium","high"]reasoning: true) ->reasoning_options = []One thing worth disclosing about the effort values
Until 2026-08-10 the Kenari endpoint derived each model's
reasoning_optionsfrom theopenrouterslice of this repo's own feed, not from the model author. Had this adapter run before that date, it would have round-tripped models.dev data back into models.dev as though it were an independent observation, and any error in the OpenRouter slice would have been laundered into theproviders/kenarirows as corroboration.That is fixed at the source. The endpoint now reads the model author's own first-party slice when the author is a provider key this repo already carries, and falls back to the OpenRouter list otherwise.
deepseek-v4-flashis the clearest case: it advertised["high","xhigh"](OpenRouter's list) and now advertises["low","high","max"], which is what DeepSeek's own slice in this repo says. #4393 hand-corrects the rows that carried the old values.So the adapter now reads a value the host derives from the author, and for a model whose author is not in this repo it still reads a value that traces back to the OpenRouter slice. That is the same provenance the hand-authored rows already had, and it is worth knowing rather than assuming every synced value is independent.
Verification
Current as of 2026-08-10:
review,validate) green on this branchFrom when the adapter was written, against the catalog as it stood then:
bun models:sync kenaritwice: second run is a no-op (0 created, 0 updated, 0 deleted)bun validatepassesbun run test: same 3 pre-existing failures as a clean checkout ofdev, none related