Skip to content

feat: split LLM Gateway into two provider catalogs - #4011

Open
smakosh wants to merge 14 commits into
anomalyco:devfrom
smakosh:feat/llmgateway-devpass-split
Open

feat: split LLM Gateway into two provider catalogs#4011
smakosh wants to merge 14 commits into
anomalyco:devfrom
smakosh:feat/llmgateway-devpass-split

Conversation

@smakosh

@smakosh smakosh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

LLM Gateway is splitting its opencode-facing catalog into two providers served by the same endpoint and API key:

  • llmgateway → renamed to "DevPass (LLM Gateway)" — everything else is unchanged: same provider id, same aggregated root-model catalog (claude-sonnet-5, gpt-5.5, …) auto-routed by the gateway, so existing user configs keep working as-is.
  • llmgateway-providers ("LLM Gateway") — new — one entry per upstream provider mapping, addressed the way the gateway accepts provider-pinned requests: provider/model-id (anthropic/claude-sonnet-5, azure/gpt-5.5, …), each with that specific deployment's pricing, context/output limits, and capabilities. Names carry the serving provider ("GPT-5.5 (Azure)") so duplicate deployments of the same model stay distinguishable.

Following the pattern of the original llmgateway sync (#2773), this PR ships the sync code plus a small seed catalog (7 representative generated entries covering the factored, full, duplicate-deployment, and capability-delta paths — dev now rejects providers with zero models, so the catalog can't start empty). The scheduled sync automation fills in the rest (~359 entries) once the gateway change is live.

Sync

The new catalog is synced by a second module in sync/providers/llmgateway.ts from GET /v1/models?mapped=true (gateway PR):

  • Mapped entries resolve their canonical base_model from the root model id + family (the serving provider prefix is unrelated to the originating lab). In a local run against the gateway build, 283 of 359 generated files factor against existing models/ metadata; the disambiguating name override is deliberately kept on factored files and carried forward on resyncs.
  • Each mapped entry ships exactly one provider mapping whose vision/tools/reasoning flags describe that specific deployment; those flags drive capability fields (as overrides on factored entries, with values equal to the base dropped), the deployment's declared reasoning_efforts translate into reasoning_options, and the gateway's served max_output takes precedence over inherited/authored output limits.
  • Guard: a deployment without the mapped view ignores the query param and returns unprefixed root ids — parseModels throws in that case instead of syncing wrong ids into the provider-pinned catalog. Until the gateway PR deploys, a scheduled sync of llmgateway-providers fails loudly rather than producing a wrong PR.
  • llmgateway/custom (BYO-model placeholder) and llmgateway/auto (the auto-router, already covered by the DevPass catalog) are excluded.
  • Schema fix along the way: context_length is optional in the gateway response (absent on pseudo-models and some non-text mappings), so the shared Zod schema and both builders now tolerate that.

Verification

  • bun validate passes with the seed catalog.
  • Unit tests cover mapped factoring (root id + kept name), capability/limit overrides from the mapping and max_output, max_output precedence on resyncs, and the unprefixed-id refusal guard.
  • A full sync run against a gateway build with the mapped view creates all 359 model files, after which bun validate still passes.

@smakosh

smakosh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Note on the failing review check: the reviewer bot can't fetch the diff because this PR exceeds GitHub's 300-file diff API limit (PullRequest.diff too_large — 364 files, 359 of them the generated providers/llmgateway-providers/ catalog). validate passes. Happy to split the code change and the generated catalog into separate PRs if you prefer.

Renames the existing llmgateway provider to "DevPass (LLM Gateway)" (id
and models unchanged: the aggregated, auto-routed root-model catalog) and
adds llmgateway-providers ("LLM Gateway"): one entry per upstream
provider mapping, addressed as provider/model-id, synced from
/v1/models?mapped=true. The catalog starts empty and is populated by the
scheduled sync automation; the sync refuses to run against a deployment
without the mapped view so it fails loudly instead of syncing wrong ids.

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@smakosh
smakosh force-pushed the feat/llmgateway-devpass-split branch from afe85a0 to fb0d722 Compare August 3, 2026 19:21
@smakosh

smakosh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Restructured to sync-code only (7 files, matching how #2773 introduced the original llmgateway sync): dropped the 359 generated catalog files — the scheduled sync automation will populate providers/llmgateway-providers/ once the gateway's mapped view is live. This also unblocks the review check, which previously failed on GitHub's 300-file diff API limit.

@smakosh smakosh closed this Aug 3, 2026
@smakosh smakosh reopened this Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:434 - Check: Mapped brand-new factored path must apply deployment capability flags. Why: PR/docs say each mapped entry’s providers[0].vision/tools/reasoning describe that deployment and the brand-new path should use them, but the canonical-base branch only passes { name, limit, cost }. ~283/359 first-sync files factor here, so deployment-specific capability deltas never land and base reasoning/tool_call/attachment are always inherited. Action: On brand-new factored creates, pass mapping-derived attachment, tool_call, and reasoning (and matching reasoning_options when reasoning) into factorBaseModel so they override base when they differ.
  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:434 - Check: Factored models should inherit base limit.output unless the API reports a real max output. Why: Brand-new factored uses output: model.max_output ?? context. When max_output is absent, output is forced to full context (e.g. 200k), overriding lab defaults (e.g. 64k). Aggregated buildLLMGatewayModel correctly leaves factored output undefined so base inherits. Action: Use only model.max_output when present; otherwise omit output on factored creates (same as the aggregated path).
  • [high] [possible mistake] packages/core/src/sync/providers/llmgateway.ts:353 - Check: Mapped sync should treat gateway max_output as authoritative for deployment limits. Why: output is existing?.limit?.output ?? model.max_output ?? context. existing is the resolved merge, so base/inherited output always wins and API max_output is ignored on updates—opposite of OpenRouter (max_completion_tokens ?? existing ?? context) and the PR goal of per-deployment limits. Action: Prefer model.max_output when present; only fall back to existing/context when the API omits it.
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts:434 - Check: Relay reasoners must not get empty reasoning_options when the host exposes controls. Why: Brand-new mapped models never set reasoning_options; the runner fills [] for reasoning = true. This host documents effort/budget controls (provider.toml), and sibling providers/llmgateway already has curated options for the same API. Empty means “no caller control,” not “unknown.” First population will stamp incorrect [] across the catalog. Action: Seed reasoning_options from the sibling root model (providers/llmgateway/models/<rootID>.toml) when present; otherwise set the real gateway control set for that model—do not rely on default [].
  • [medium] [violation] packages/core/test/sync.test.ts:35 - Check: New sync behavior needs focused regression tests. Why: buildLLMGatewayMappedModel and the mapped parseModels guard are large new paths; only buildLLMGatewayModel is covered. Bugs above (root-ID factoring, mapping flags, max_output, unprefixed-ID refusal) can ship unnoticed. Action: Add tests for mapped factoring (root ID + name), capability/limit overrides from providers[0]/max_output, and refusal when the mapped view returns unprefixed IDs.

Addresses the PR review: brand-new factored mapped entries now carry the
mapping's own capability flags (attachment/tool_call/reasoning and
structured_output) as overrides, translate the deployment's declared
reasoning_efforts into reasoning_options instead of stamping [], prefer
the gateway's served max_output over inherited/authored output limits,
and only fall back to context when the base metadata declares no output.
Adds unit tests for mapped factoring, capability overrides, max_output
preference, and the unprefixed-id refusal guard.

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@smakosh

smakosh commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review items in f5678d9:

  • Capability flags on factored creates: brand-new factored entries now pass the mapping's vision/tools/reasoning (and structured_output) into factorBaseModel, so deployment deltas land as overrides and values equal to the base are dropped as before.
  • Factored output limit: model.max_output now wins whenever the gateway reports it (creates and resyncs, matching OpenRouter's precedence); when absent, factored creates inherit the base's limit.output and only fall back to context when the base metadata declares no output (output is required downstream).
  • reasoning_options: rather than seeding from the sibling llmgateway files, the mapped view exposes each deployment's exact accepted reasoning_efforts, so those translate directly into [{ type = "effort", values = [...] }] — deployment-accurate (e.g. Anthropic mappings get low…max, Azure OpenAI none…xhigh). Mappings that declare no effort values keep the runner's [] behavior, same as the rest of the catalog.
  • Tests: added unit tests for mapped root-ID factoring (name kept), capability/limit overrides from providers[0]/max_output, max_output preference on resyncs, and the unprefixed-id refusal guard.

Verified by regenerating the full catalog locally against the gateway build: 359 files, bun validate passes, and the sync test suite passes (the one failing test, DeepInfra preserves live modalities…, fails identically on upstream dev without this branch).

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:167 - Check: Sync modules must not treat a failed/empty upstream payload as an authoritative empty catalog (sync.md deletion behavior). Why: data.every((model) => model.id.includes("/")) is vacuously true for [], so a 200 with no models (or only llmgateway/* / non-text rows that are later filtered out) passes the mapped-view guard and the runner deletes every local llmgateway-providers model. Action: Fail closed when the mapped payload is empty, or when the post-filter set is empty (e.g. require data.length > 0 and a non-empty filtered result before proceeding).
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts:375 - Check: When deployment flags are treated as authoritative, resyncs must refresh them (not only first create). Why: buildLLMGatewayMappedModel applies mapping.vision / tools / reasoning, reasoning_effortsreasoning_options, and structured_outputs only on the brand-new branches. Existing factored/full paths still freeze prior attachment / tool_call / reasoning / options and only refresh cost+limit, so a wrong first write or a later API change never corrects the catalog—despite the PR treating those mapping fields as the reliable per-deployment source. Action: On existing paths, refresh capability fields and reasoning_options from the mapping when present (same approach as OpenRouter/live peers), while still preserving true hand overrides only when the API omits the signal.
  • [medium] [possible mistake] packages/core/src/sync/providers/llmgateway.ts:358 - Check: Mapped entries are documented as carrying exactly one provider mapping. Why: The code always uses model.providers?.[0] with no length check. Missing/empty providers silently falls back to noisy supported_parameters; multiple mappings silently pick an arbitrary first row for capabilities. Action: Require providers?.length === 1 (or explicitly define merge/skip behavior) before trusting providers[0].

smakosh added 2 commits August 4, 2026 18:55
The dev branch now rejects providers with zero models, so the empty
.gitkeep-anchored catalog no longer validates. Seed it with a small
representative set generated by the sync (factored, full, duplicate
deployments of one model, capability deltas); the scheduled sync fills
in the rest once the gateway's mapped view is live.

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:379 - Check: Factored mapped resyncs must not invent limit.output when the gateway omits max_output and the file inherits output from base_model. Why: buildLLMGatewayMappedModel builds limit.output as model.max_output ?? existing?.limit?.output ?? context for the existing-factored path. Seeded factored files such as anthropic/claude-opus-4-8, anthropic/claude-sonnet-5, and */gpt-5.5 intentionally omit limit.output so they inherit lab output. On the next sync, if max_output is absent, this stamps output = context and permanently overrides the base (e.g. 128k → 1M). The brand-new path already avoids this via canonicalOutputLimit; the resync path does not. Action: Mirror the brand-new factored logic on resync: prefer max_output, else keep authored output, else leave output unset when the canonical base declares one (only fall back to context when the base has no output).
  • [high] [violation] providers/llmgateway-providers/models/perplexity/sonar-pro.toml:1 - Check: Non-lab hosts must use base_model when the lab model is nameable and models/<lab>/<model>.toml exists. Why: Perplexity Sonar Pro already has complete lab metadata at models/perplexity/sonar-pro.toml, and the sibling aggregated catalog uses base_model = "perplexity/sonar-pro". This file is a full inline definition instead, and it also diverges from that lab entry (attachment = false vs true, text-only input vs text+image, output = 200_000 vs 8_192). Action: Replace the inline definition with an override-only file pointing at perplexity/sonar-pro (keep provider name/cost and only real deployment deltas). Fix the sync path that failed to factor this id (missing/wrong family or resolver miss) so future syncs do not re-expand it.
  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:361 - Check: Multi-model relay reasoning_options must follow this host’s real controls and must not stamp [] just because effort metadata is missing when lab/same-surface peers expose controls. Why: Mapped sync only emits { type = "effort", values = reasoning_efforts } and otherwise leaves options unset, so the runner defaults new reasoners to []. Seeded embercloud/glm-5.1 therefore has reasoning_options = [] while first-party Zhipu/ZAI and the same gateway’s aggregated providers/llmgateway/models/glm-5.1.toml use toggle (plus interleaved). That is the empty-from-uncertainty anti-pattern for a reasoner on this unified gateway surface. Action: When reasoning_efforts is absent/empty, do not invent [] for models whose same-host/lab baseline is toggle (or other non-effort controls). Author toggle (with a leading wire comment) or preserve/copy the aggregated llmgateway options for that root model; update the glm-5.1 seed accordingly.
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts:400 - Check: Mapped sync treats deployment reasoning_efforts as authoritative for new entries and should keep them current on resync. Why: The existing-factored branch refreshes cost/limit/name but never passes reasoning_options from mapping.reasoning_efforts. After the first write, the runner only preserves whatever was already on disk, so later API effort changes never apply and a bad first stamp (e.g. []) is sticky. Action: On existing factored/full paths, set reasoning_options from reasoning_efforts when present; when absent, preserve existing options (so hand-curated toggle/budget is not wiped).
  • [medium] [possible mistake] providers/llmgateway-providers/models/together-ai/kimi-k2.6.toml:7 - Check: Effort lists on this gateway should match the deployment’s real accepted values and same-host peers for the model. Why: This seed allows only effort = none while reasoning still inherits true from moonshotai/kimi-k2.6. The same gateway’s aggregated kimi-k2.6 entry documents a full none|minimal|low|medium|high|xhigh|max set, and the Moonshot lab entry uses toggle. ["none"] alone is an odd control surface (effectively “off only”) for a reasoner. Action: Verify Together’s mapped reasoning_efforts (and whether reasoning is actually enabled). Align to the real set—or set reasoning = false / drop the lone none effort if the deployment does not expose reasoning controls.

Round 2 of review feedback:
- Factored resyncs no longer stamp context as limit.output when the
  gateway omits max_output and the base declares an output to inherit;
  the served max_output still wins whenever reported (creates and
  resyncs), and reasoning_options now refresh from deployment efforts.
- A deployment whose only accepted effort is "none" is a plain on/off
  switch, so it translates to a toggle (matches the lab's control).
- When a deployment declares no efforts, mapped entries reuse the
  aggregated llmgateway catalog's curated reasoning_options for the
  same root model instead of ending up with []; a curated [] counts as
  unknown so a bad first stamp is not sticky. The runner also stops
  stamping [] onto factored reasoners whose base metadata already
  declares reasoning_options (it would shadow the base's controls).
- perplexity added to the canonical prefixes so Sonar models factor
  against their lab metadata; the sonar-pro seed is now override-only.

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@smakosh

smakosh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Round 2 addressed in 48bf376:

  • Factored resync output: now mirrors the create path — served max_output wins when reported; otherwise the authored output is kept, and when neither exists the file keeps inheriting the base's limit.output (context fallback only when the base declares none). Covered by a test asserting no limit override is invented on such resyncs.
  • sonar-pro factoring: root cause was the canonical resolver — perplexity was missing from CANONICAL_PROVIDER_PREFIXES, so Sonar ids never matched their lab metadata (this also lets OpenRouter's perplexity/* ids factor). The seed is now override-only (base_model = "perplexity/sonar-pro" + name/cost and the real deployment deltas).
  • reasoning_options: three-level fallback now — deployment-declared reasoning_efforts win; else non-empty curation on the file; else the aggregated llmgateway catalog's curated options for the same root model (same gateway surface). A curated [] counts as unknown, so a bad first stamp heals on resync. Additionally the runner no longer stamps [] onto factored reasoners whose base metadata declares reasoning_options — the empty array would shadow the base's real controls at generate time (the existing preserveReasoningOptions default-[] test still passes; the new behavior only kicks in when the base declares options). Across the full catalog this drops empty-[] files from 128 to 36 (models with no declared efforts, no sibling curation, and no base options). embercloud/glm-5.1 now inherits toggle.
  • together-ai/kimi-k2.6: verified against the gateway's catalog — Together's deployment accepts any effort string without validating it and no tier changes reasoning length; the only honoured control is none via the thinking switch. That is a plain on/off switch, so a ["none"]-only effort list now translates to { type = "toggle" }, matching the Moonshot lab entry.
  • Resync effort refresh: existing factored/full paths now update reasoning_options from reasoning_efforts when present and preserve curated options otherwise.

Verified with a full local regeneration (359 files, bun validate green) and the sync test suite (117 pass; the DeepInfra modalities test fails identically on dev without this branch).

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:167 - Check: Mapped sync must not authoritatively delete the catalog on an empty or fully filtered response. Why: data.every((m) => m.id.includes("/")) is true for [], and after filtering llmgateway/* / non-text the desired set can also be empty. With default deleteMissing, that wipes the seed catalog (and later the full mapped tree) instead of failing loudly like the unprefixed-id guard. Action: Reject empty raw data and an empty post-filter set (throw), same spirit as the unprefixed-id check.
  • [medium] [possible mistake] providers/llmgateway-providers/models/openai/gpt-5.5.toml:8 - Check: Provider cost must include context tiers when this host/deployment uses them. Why: First-party providers/openai/models/gpt-5.5.toml and same-surface providers/llmgateway/models/gpt-5.5.toml both author [[cost.tiers]] at 272k (10/45/1). Mapped cost only preserves existing?.cost?.tiers and never invents tiers, so these seeds (and any later first-create sync) publish flat 5/30 pricing and understate large-context cost. Action: Verify gateway OpenAI (and Azure, if identical) pricing; if tiers apply, add the 272k tier to openai/gpt-5.5.toml and azure/gpt-5.5.toml so resync preserves them.
  • [medium] [violation] providers/llmgateway-providers/models/perplexity/sonar-pro.toml:3 - Check: attachment and [modalities].input must stay consistent after base_model merge. Why: Seed sets attachment = false while inheriting lab modalities.input = ["text", "image"], so the resolved model claims image input but no attachments. buildLLMGatewayMappedModel only maps mapping.visionattachment and never overrides modalities when vision is off, so this will recur for other vision-capable bases. Action: When vision is false, also override modalities (drop non-text inputs, or match architecture); fix the sonar-pro seed the same way.
  • [medium] [violation] providers/llmgateway-providers/models/embercloud/glm-5.1.toml:5 - Check: Every toggle needs a leading top-of-file wire-path comment (AGENTS.md → Reasoning options). Why: embercloud/glm-5.1.toml and together-ai/kimi-k2.6.toml ship type = "toggle" with no leading comment; provider.toml docs are not a substitute for the per-file requirement, and sync preserves only leading headers. Action: Add a leading comment on both toggle seeds documenting the exact gateway control (e.g. thinking / reasoning_effort=none path used for on/off).
  • [medium] [possible mistake] providers/llmgateway-providers/models/embercloud/glm-5.1.toml:1 - Check: Same-host reasoners that emit a reasoning side channel should set interleaved. Why: Lab and same-endpoint peers (providers/zhipuai/models/glm-5.1.toml, providers/llmgateway/models/glm-5.1.toml, and the kimi-k2.6 counterparts) set [interleaved] field = "reasoning_content". Mapped seeds omit it, and brand-new factored sync never copies it, so clients miss the side channel on this API. Action: Add interleaved to the glm-5.1 and kimi-k2.6 seeds (and consider preserving/copying it in mapped sync from sibling aggregated entries when present).

Round 3 of review feedback:
- Both LLM Gateway syncs now reject an empty (or fully filtered)
  response instead of authoritatively deleting the catalog through the
  delete-missing pass; the every() prefix guard alone passed on [].
- A vision-less deployment also overrides modalities on factored
  creates, so attachment=false can no longer coexist with inherited
  image input (sonar-pro seed regenerated accordingly).
- Mapped entries copy the interleaved reasoning side-channel from the
  aggregated llmgateway catalog when the deployment reasons (same wire
  surface); glm-5.1 and kimi-k2.6 seeds now carry it.
- Toggle seeds carry the required leading wire-path comment.
- gpt-5.5 seeds author the 272k context pricing tier so resync
  preserves it, matching the first-party and aggregated entries.

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@smakosh

smakosh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in 545d19a:

  • Empty-response wipe: both syncs now throw on an empty raw response, and the mapped sync also throws when filtering leaves nothing — same spirit as the unprefixed-id guard, so the delete-missing pass can never be fed an empty desired set. Test added covering both.
  • attachment vs modalities: factored creates from a vision-less deployment now override modalities with the deployment's own (text-only) inputs, so attachment = false can't coexist with inherited image input. The sonar-pro seed is regenerated accordingly (input = ["text"]).
  • interleaved: mapped entries now copy the interleaved side-channel from the aggregated llmgateway entry for the same root model when the deployment reasons (same wire surface; existing curation wins on resyncs). glm-5.1 and kimi-k2.6 seeds carry field = "reasoning_content".
  • Toggle comments: both toggle seeds now have the leading wire-path header (reasoning_effort = "none" → deployment thinking switch; headers survive resync).
  • gpt-5.5 tiers — good catch, and it went deeper than the seeds: the gateway's own catalog supports context-tiered billing and gpt-5.6-sol declares exactly this 272k tier, but gpt-5.5 predates that support and was never backfilled — i.e. the gateway currently bills long-context gpt-5.5 flat. We're fixing the gateway catalog (10/45, cache read 1.00 over 272k, both OpenAI and Azure), and the seeds here now author the same [[cost.tiers]] so resync preserves them.

bun validate green, 118 sync tests pass (DeepInfra modalities failure is pre-existing on dev).

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:420 - Check: Optional context_length must not author limit.context = 0. Why: Mapped (and aggregated) translation does const reported = model.context_length ?? 0 then context = reported > 0 ? reported : existing?.limit?.context ?? reported. On a brand-new model with missing/zero context_length, existing is undefined, so context becomes 0 and is written into [limit] (and often output via the same fallback). That overrides a real base context with an unusable window and can pass schema (min(0)). Action: When no positive served context exists, leave limit.context unset on factored creates (inherit base) and do not fall back to 0; refuse or skip unfactored creates that lack a positive context rather than publishing zero limits.
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts:547 - Check: vision === false must not leave image/pdf input modalities. Why: Brand-new factored path sets modalities: mapping?.vision === false ? defaultModalities(model) : undefined, which trusts architecture.input_modalities. If the API keeps model-level image/pdf inputs while the deployment flag is vision: false, the file still gets attachment = false with image/pdf input—the contradiction the comment says to prevent. The full (unfactored) create path never applies this guard at all (attachment from mapping.vision, modalities always from architecture). Action: When mapping.vision === false, force non-image input (e.g. text-only, or strip image/pdf) on both factored and full create paths; add a test where architecture still lists image but vision is false.

Round 4 of review feedback:
- A missing/zero context_length is no longer written as limit.context=0:
  factored entries leave context unset and inherit the base, and
  unfactored creates without a positive served context are skipped
  (reported via sourceID) instead of publishing unusable limits. Applies
  to both the aggregated and mapped builders.
- vision=false now forces non-image input modalities from the mapping
  itself instead of trusting the model-level architecture, on both the
  factored and unfactored create paths (and the existing-full fallback).

Claude-Session: https://claude.ai/code/session_017pReWhniXJcDL9aiQHqoFQ
@smakosh

smakosh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Round 4 addressed in 7ddf9e9:

  • Zero limits: a missing/zero context_length can no longer be authored as limit.context = 0 in either builder. Factored entries (creates and resyncs) leave context unset and inherit the base; unfactored creates without a positive served context are skipped via translateModel returning undefined (and reported through sourceID) instead of publishing unusable limits. The output fallbacks follow the same rule, so no path can cascade a 0 into limit.output either.
  • vision vs modalities: vision: false now forces non-image input from the mapping itself (deploymentModalities strips image/pdf, falling back to ["text"]) rather than trusting the model-level architecture, on the factored create path, the unfactored create path, and the existing-full fallback alike.
  • Tests: skipped-create (both builders), factored create without served context authors no limit at all, and a vision-false-with-image-architecture case asserting text-only input on both create paths.

Also verified end-to-end: running the sync over the committed seed files leaves all 7 byte-identical (leading toggle headers and the authored [[cost.tiers]] survive resync) while creating the other 352, and bun validate passes. 121 sync tests pass (DeepInfra modalities failure remains pre-existing on dev).

@rekram1-node

Copy link
Copy Markdown
Collaborator

These are the provider names you want to go with? They seem a bit interesting.... Not intuitive.

@smakosh

smakosh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

These are the provider names you want to go with? They seem a bit interesting.... Not intuitive.

We want to have two providers:

  • LLM Gateway
  • DevPass By LLM Gateway

one is model IDs only and the other has models mapped by providers.

…ass-split

# Conflicts:
#	packages/core/test/sync.test.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [violation] providers/llmgateway-providers/logo.svg:1 - Check: New provider logo.svg must use currentColor with no fixed size or hardcoded colors. Why: The root <svg> sets width="24" height="24", which violates the new-provider logo rules in AGENTS.md (merge blocker). Action: Drop the fixed width/height attributes and keep a square viewBox with currentColor fills only (same shape as the compliant examples in AGENTS.md).
  • [medium] [possible mistake] packages/core/src/sync/providers/llmgateway.ts (mapped parseModels / buildLLMGatewayMappedModel) - Check: Mapped catalog entries are defined as one upstream mapping whose vision/tools/reasoning/reasoning_efforts drive capabilities and reasoning_options. Why: parseModels only rejects empty/unprefixed payloads; it still accepts mapped IDs with missing or empty providers, and the builder then silently falls back to noisy supported_parameters / sibling defaults. That can author wrong capability overrides or [] reasoning controls for the provider-pinned catalog. Action: Fail the mapped sync when any kept model lacks exactly one providers[] entry (same fail-loud style as the unprefixed-id guard), and add a unit test for that case.

Review round 5: drop the fixed width/height from the new provider logo
(AGENTS.md blocker), and fail the mapped sync loudly when a kept model
does not carry exactly one providers[] mapping instead of letting the
builder silently fall back to noisy supported_parameters defaults.

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts - Check: Override-only base_model files must not restate or invent identical/synthetic lab metadata on resync. Why: Brand-new mapped factored creates intentionally author only a disambiguating name (no description), but the existing-factored path does existing.description ?? describeModel(...). On the first automation resync, describeModel synthesizes a generic blurb (e.g. “Flagship reasoning model…”) that differs from the lab description and gets written as a sticky override on every name-pinned mapped entry. Action: On the existing-factored path, pass only description: existing.description (no describeModel fallback), matching other factored providers (e.g. merge-gateway/cortecs); leave description unset so the lab text keeps inheriting.
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts - Check: Every toggle reasoning control needs a leading top-of-file wire-path comment (AGENTS.md → Reasoning options). Why: reasoning_efforts: ["none"] is translated to { type: "toggle" } with no mechanism to attach a header. The seed toggle files include the required comment, but bulk creates from the scheduled mapped sync will emit headerless toggles; sync only preserves an existing header, it never creates one. Action: When emitting toggle (none-only efforts or sibling toggle), ensure new files get a leading wire comment (e.g. default header for this provider’s $.reasoning_effort = "none" path), or avoid writing toggle until that header can be authored.

Review round 6: mapped factored resyncs no longer stamp a synthesized
describeModel blurb as a sticky description override (unset keeps
inheriting the lab text, matching merge-gateway/cortecs), and mapped
sync writes now author the required leading wire-path comment on files
that carry a toggle reasoning control via a new optional header on the
translateModel result (an existing on-disk header always wins).

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts - Check: Mapped factored resyncs must keep deployment capability flags aligned with providers[] (same authority as brand-new creates / merge-gateway). Why: On existing base_model entries the builder freezes resolved reasoning / attachment / tool_call / structured_output / modalities from the prior file, but still refreshes reasoning_options from live mapping.reasoning + efforts. If a deployment later reports reasoning: false (or the reverse), options are cleared or filled while the boolean stays stuck, so preserveReasoningOptions can stamp [] on a non-reasoner or drop controls on a reasoner. Hourly resyncs will not correct the seed catalog or any later drift. Action: On the existing-factored path, apply mapping flags the same way as the create path (reasoning/vision/tools/structured_outputs and vision-driven modalities), or freeze reasoning_options with the other curated fields; do not refresh options from a mapping.reasoning value you are not also writing.
  • [low] [possible mistake] providers/llmgateway-providers/models/together-ai/kimi-k2.6.toml:8 - Check: Provider-pinned capabilities/limits should match the upstream Together deployment this id represents. Why: Seed sets tool_call = false, structured_output = false, and limit.output = 32_768, while first-party providers/togetherai/models/moonshotai/Kimi-K2.6.toml has tools/structured output enabled and output = 131_000, and the seed’s unit prices match that Together row. That combination looks like a bad mapping dump rather than a real Together pin. Action: Re-check providers[].tools / structured_outputs / max_output for together-ai/kimi-k2.6 on the mapped catalog and correct the seed (or cite why this pin is intentionally weaker than Together’s own API).

Review round 7: mapped existing-entry resyncs (factored and full) now
apply the deployment mapping's reasoning/vision/tools/structured-output
flags with the same authority as creates, so the written booleans and
the reasoning_options derived from them always move together and drift
self-heals hourly; prior curation only fills in where the mapping is
silent. Also documents in the together-ai/kimi-k2.6 seed header why
that pin is intentionally weaker than Together's first-party row (the
gateway serves it with tools/JSON off and a 32k output cap per its own
e2e'd catalog mapping).

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts - Check: Mapped resyncs must realign vision-related modalities in both directions (PR claims deployment flags stay authoritative on resyncs; attachment/image input must stay consistent). Why: Factored and full resync paths only force text-only modalities when mapping.vision === false; when vision later becomes true they keep existing.modalities. A prior no-vision override (e.g. input = ["text"]) therefore stays sticky after vision returns, so the file can keep text-only input while attachment flips back to true/inherited vision — the inverse of the contradiction the sync already tries to prevent. Hourly resync will not self-heal. Check: mapping?.vision === false ? deploymentModalities(...) : existing.modalities. Action: When mapping.vision === true (or whenever vision is present), recompute modalities via deploymentModalities(model, mapping.vision) or clear the override so base image/pdf inputs can inherit again; add a resync test for vision false → true.

Review round 8: mapped resyncs no longer keep a stale text-only
modalities override once the deployment's vision returns — a declared
vision=true clears the override on factored entries (base image/pdf
inputs inherit again) and recomputes from the served architecture on
full entries, mirroring how vision=false already strips them; only a
silent mapping leaves curated modalities untouched.

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [possible mistake] packages/core/src/sync/providers/openrouter.ts:37 - Check: Shared canonical prefix tables must not change unrelated providers without intent. Why: Adding perplexity to CANONICAL_PROVIDER_PREFIXES makes resolveCanonicalBaseModel("perplexity/…") succeed for every caller (OpenRouter, Kilo, Vercel, Merge Gateway, …). Today those hosts still ship full standalone Perplexity TOMLs (e.g. providers/openrouter/models/perplexity/sonar-pro.toml); the next automation sync will start factoring them against models/perplexity/*, which is an unstated cross-provider behavior change. LLM Gateway can resolve the same lab IDs via resolveModelMetadataBaseModel (exact models/ path match) without widening the OpenRouter prefix map. Action: Prefer resolveModelMetadataBaseModel (or a llmgateway-local alias) for mapped root IDs; only keep the shared prefix if you intentionally want OpenRouter/Kilo/etc. to factor Perplexity on the next sync and call that out.
  • [medium] [violation] packages/core/src/sync/providers/llmgateway.ts:294 - Check: Missing/zero context_length must never be authored as limit.context = 0 (stated goal of the zero-context fix). Why: Both builders compute servedContext correctly, then the existing-full limit object still does context: context ?? reported (and output: … ?? reported). When the API omits/zeros context and the existing full row has no limit.context, reported is 0/undefined and the file is rewritten with limit.context = 0 (schema min(0) allows it). Factored paths avoid this; the full-model resync path undoes the guard. Action: Stop falling back to reported for authored limits—use only a positive context/servedOutput, and skip or preserve the existing full row when neither the API nor the file provides a usable context.

Review round 9: drop the perplexity entry from the shared
CANONICAL_PROVIDER_PREFIXES (it would silently start factoring other
hosts' standalone perplexity files) — the llmgateway sync now resolves
lab IDs through resolveModelMetadataBaseModel, whose exact models/ path
match covers perplexity without touching other providers. Full-row
resyncs in both builders no longer fall back to the zero/absent
reported context: authored limits only ever carry known-positive
values, an authored 0 on disk counts as unusable, and a full row with
no usable context anywhere fails loudly (skipping would hand the file
to the delete-missing pass).

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] packages/core/src/sync/providers/llmgateway.ts:493 - Check: Mapped reasoning_options must keep this host’s full control surface (effort and any real budget_tokens), and resync must not wipe hand-authored non-effort controls. Why: When providers[].reasoning_efforts is non-empty, the builder sets reasoning_options to only that effort list (or a lone toggle for ["none"]) and short-circuits past existing/sibling options. The same gateway surface already curates budget_tokens on aggregated peers (e.g. claude-sonnet-4-6, claude-opus-4-5, gemini-2.5-pro) and documents $.reasoning.max_tokens in provider.toml. The bulk mapped sync will therefore omit budgets on create and erase them on every later resync whenever efforts are present. DigitalOcean’s sync already preserves non-effort options when applying remote efforts. Action: When applying deployment efforts, merge them with non-effort options from existing and/or the aggregated sibling (keep budget_tokens; replace only the effort/toggle entry), and add a regression test for a sibling/existing budget_tokens + remote reasoning_efforts case.
  • [medium] [possible mistake] packages/core/src/sync/providers/llmgateway.ts:500 - Check: Brand-new mapped creates should not drop known gateway context pricing tiers that the DevPass sibling already authors. Why: cost.tiers is only taken from existing?.cost?.tiers, never from the aggregated llmgateway sibling. GPT-5.5 seeds were hand-fixed with the 272k tier, but the scheduled full sync (~359 files) will create other tiered models (GPT-5.5-pro, Grok 4.x, Gemini, etc.) without those tiers, understating long-context price until someone re-authors them. Action: Seed cost.tiers from the aggregated sibling (or equivalent curated source) on create when the API does not supply tiers; preserve existing tiers on resync as today.

Review round 10: deployment reasoning_efforts now own only the
effort/toggle surface — curated non-effort controls such as
budget_tokens (the same host's $.reasoning.max_tokens path, mirroring
DigitalOcean's sync) survive from the existing file or the aggregated
sibling instead of being wiped on every resync. Mapped creates also
seed cost.tiers from the aggregated sibling's curated tiers, since the
gateway API exposes none and the bulk sync would otherwise author
tiered models at flat long-context rates; authored tiers still win on
resync.

Claude-Session: https://claude.ai/code/session_0131ZfUnTfJrCzygw3wE4bNF
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

I've finished reviewing the split provider, sync module, seeds, logo, and reasoning-option handling against the project rules.

@smakosh

smakosh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Should be ready now @rekram1-node

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.

2 participants