Skip to content

feat(sync): add Merge Gateway model sync#3249

Open
MatthewFeroz wants to merge 4 commits into
anomalyco:devfrom
MatthewFeroz:merge-model-sync
Open

feat(sync): add Merge Gateway model sync#3249
MatthewFeroz wants to merge 4 commits into
anomalyco:devfrom
MatthewFeroz:merge-model-sync

Conversation

@MatthewFeroz

@MatthewFeroz MatthewFeroz commented Jul 13, 2026

Copy link
Copy Markdown

Summary

  • add an authenticated Merge Gateway model sync using the paginated GET /v1/models catalog
  • map live pricing, limits, modalities, and capabilities while preserving curated provider-specific metadata
  • factor resolvable models through canonical base_model metadata
  • retain models missing from API-key-scoped responses and report models without canonical metadata
  • register Merge Gateway with the shared sync runner and hourly aggregator workflow
  • bootstrap the Merge Gateway catalog from the live API
  • preserve curated cache pricing while the public endpoint omits it, and ingest route-level prompt_caching / cache-price fields authoritatively when Merge exposes them
  • cross-check cache-capable routes against matching provider catalog entries, increasing cache-price coverage from 62 to 75 of 137 model files and correcting GPT-5.6 Sol to the route's $0.375/M cache-read rate
  • inherit canonical names when Merge returns an upstream org/model ID as display_name

The initial live sync created 43 model entries, updated 66, and deleted none.

Testing

  • bun models:sync merge-gateway --dry-run
    • 0 created, 0 updated, 0 removed; 137 unchanged after regeneration
  • bun test packages/core/test/sync.test.ts
    • 52 passed, 0 failed
  • bun validate
  • git diff --check
  • loaded the generated catalog through a local OpenCode checkout and confirmed merge-gateway/openai/gpt-5.6-sol, merge-gateway/anthropic/claude-fable-5, merge-gateway/xai/grok-4.5, etc.

Sources

Maintainer setup

The scheduled workflow requires a regular Merge Gateway API key configured as the repository Actions secret MERGE_GATEWAY_API_KEY. The credential is not included in this PR.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [low] [possible mistake] providers/merge-gateway/models/minimax/minimax-m2.toml:2 (also providers/merge-gateway/models/qwen/qwen3-coder-480b-a35b-instruct.toml:2, providers/merge-gateway/models/qwen/qwen3-next-80b-a3b-thinking.toml:2) - Check: name must be a human-readable display name, not a model ID. Why: These files set name = "MiniMaxAI/MiniMax-M2", name = "Qwen/Qwen3-Coder-480B-A35B-Instruct", and name = "Qwen/Qwen3-Next-80B-A3B-Thinking"org/model-slug strings that look like raw IDs and are inconsistent with sibling entries (e.g. minimax-m2.1 uses "MiniMax M2.1", and the canonical metadata names are clean). The sync writes model.display_name from the API, so these appear to be ID-shaped display names leaked from the catalog response; leaving them produces an ugly user-facing model name. Action: Verify the API display_name for these three models and either replace each with a proper display name or drop the name override so the clean models/ metadata name inherits.
  • [low] [possible mistake] packages/core/src/sync/index.ts:142 - Check: New sync providers should be added to a groups entry per sync.md "Adding A Provider" step 5. Why: merge-gateway is registered in providers but is absent from groups.aggregators, even though it is a multi-vendor gateway like llmgateway, kilo, and openrouter already in that group. bun models:sync aggregators will therefore not sync Merge Gateway, and the omission is not clearly covered by the "keep ambiguous sources out of shared groups" guidance (this source is a structured vendor catalog). Action: Add "merge-gateway" to the aggregators group, or confirm the omission is intentional.

@aakash-gupte

Copy link
Copy Markdown
Contributor

@rekram1-node can you take a look when you get a second?

@MatthewFeroz

Copy link
Copy Markdown
Author

Addressed the automated review and cache-pricing follow-up in 0ec6a52:

  • added merge-gateway to groups.aggregators
  • ID-shaped API display names now inherit clean canonical names; refreshed the affected TOMLs
  • added current route-backed cache prices for 14 Merge models
  • corrected GPT-5.6 Sol to Merge's 0.375 cache-read price and removed the unsupported cache-write price
  • added forward-compatible parsing for route-level prompt_caching/cache pricing, while preserving curated values until the public endpoint exposes those fields

Verification: 51 focused tests pass, bun validate passes, and a second live sync dry-run is clean (0/0/0, 137 unchanged).

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [possible mistake] providers/merge-gateway/models/openai/gpt-5-chat-latest.toml - Check: The merged catalog entry's limit.input must not exceed limit.context. Why: The sync adds [limit] context = 128_000, but the base model openai/gpt-5-chat-latest declares limit.input = 272_000. After base_model deep-merge, the generated entry will have input (272_000) greater than context (128_000), which is logically impossible and will mislead consumers. The shared baseModelOmit logic does not add ["limit.input"] here because limit.input is resolved from the existing (base-merged) TOML, so it is non-undefined and the omit condition never fires. Action: Verify the Merge Gateway API's context_window value for gpt-5-chat-latest (128K is below the 400K context documented in the base metadata, while the sibling gpt-5.1/5.2/5.3-chat-latest models do use 128K). If 128K is the correct provider context, add base_model_omit = ["limit.input"] so the stale 272K input limit is not inherited.

@MatthewFeroz

Copy link
Copy Markdown
Author

Fixed the follow-up reviewer finding in 31a3612: the Merge sync now distinguishes authored provider limits from base-model-resolved limits, so a smaller live context causes limit.input to be omitted instead of retaining an impossible inherited cap. Regeneration added base_model_omit = ["limit.input"] to gpt-5-chat-latest; the new regression test passes, the live dry-run is clean (137 unchanged), and full validation passes.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [violation] providers/merge-gateway/models/openai/gpt-5.6-luna.toml:2, gpt-5.6-terra.toml:2, gpt-oss-120b.toml:4, gpt-oss-20b.toml:2, anthropic/claude-3-7-sonnet-20250219.toml:4, claude-fable-5.toml:3, claude-sonnet-5.toml:3 - Check: reasoning_options on reasoning = true Merge Gateway models must reflect the controls that Merge Gateway's own routes expose, not default to []. Why: providers/merge-gateway/provider.toml documents that the OpenAI-compatible route passes through native reasoning_effort and the Anthropic-compatible route passes through thinking/output_config.effort. The same PR hand-curated openai/gpt-5.6-sol.toml with effort options (citing the OpenAI blog), and the provider's existing Anthropic entries (claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5, etc.) all declare toggle/budget_tokens/effort. OpenAI first-party, OpenRouter, Groq, and Cerebras declare the matching effort values for these exact models. Leaving these seven new entries at reasoning_options = [] claims "no verified caller control" on routes that the provider documentation and sibling entries show do expose one, so the published metadata understates Merge Gateway's API surface. Action: Apply the matching reasoning_options to these models (effort ["none","low","medium","high","xhigh","max"] for the GPT-5.6 pair, effort ["low","medium","high"] for the gpt-oss pair, and toggle/effort/budget_tokens matching the provider's other Anthropic entries for the three Claude models), or verify and document that those specific routes do not expose the documented reasoning controls.

  • [low] [possible mistake] packages/core/src/sync/providers/merge-gateway.ts:260 - Check: mergeGatewayCachePricing preserves curated cache prices only when the API exposes no cache fields at all; when the API exposes prompt_caching or one cache price, it returns undefined for whichever side the API omits rather than falling back to the existing curated value. Why: sync.md states that exposed route-level cache values "authoritatively replace preserved cache prices," which reads as only the exposed values replacing — but the code also wipes the unexposed side. The syncs authoritative Merge Gateway cache pricing test confirms a curated cache_write of 6.25 is dropped when the API exposes prompt_caching + cache_read but no cache_write. If Merge ever partially populates cache pricing, future syncs will silently delete known cache prices. Action: Confirm the Merge API always populates cache_read and cache_write together (and never returns prompt_caching without prices), or fall back to existing?.cost?.cache_read/cache_write for whichever side the API omits.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [medium] [possible mistake] packages/core/src/sync/providers/merge-gateway.ts:495 - Check: mergeGatewayCachePricing should preserve curated cache prices for fields the API does not expose, per the sync.md documentation added in this PR. Why: The authoritative flag is true whenever a prompt_caching object exists or any cache pricing field is present, making replacement all-or-nothing. When the API exposes cache_read but not cache_write (as the test at sync.test.ts:601 explicitly verifies), the existing curated cache_write is silently dropped rather than preserved. The added sync.md states "Cache prices curated before that API support remain in provider TOMLs and survive syncs," which contradicts this behavior. If the API ever returns prompt_caching with only mode and no cost fields, all curated cache pricing would be dropped. Action: Either preserve non-exposed cache prices field-by-field (only replace fields the API actually provides) or update sync.md to document the all-or-nothing replacement behavior so the code and documentation agree.
  • [medium] [possible mistake] providers/merge-gateway/models/anthropic/claude-fable-5.toml:1 - Check: The reasoning value must match what the Merge Gateway route actually supports. Why: The leading comment states "the only available Anthropic route reports supports_reasoning = false," yet the model inherits reasoning = true from the base model (models/anthropic/claude-fable-5.toml has reasoning = true) and declares reasoning_options = []. The gpt-oss comments in this same PR distinguish supports_reasoning = false (no reasoning) from configurable = false (reasoning without caller control) as separate scenarios. If supports_reasoning = false means the route does not support reasoning at all, then reasoning = true is factually wrong for this provider model and reasoning_options must be removed (the schema forbids reasoning_options when reasoning = false). Action: Verify the Merge Gateway API semantics of supports_reasoning = false. If it means the model cannot reason through this route, override reasoning = false and remove reasoning_options = []. If it only means "no caller control," confirm that interpretation is consistent with how the gpt-oss comments use the same field.

@github-actions

Copy link
Copy Markdown
Contributor

No actionable findings.

@rekram1-node rekram1-node left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action items

  • [medium] [possible mistake] packages/core/src/sync/providers/merge-gateway.ts:206 - Check: Apply the selected vendor route's supports_reasoning signal instead of always preserving resolved reasoning metadata. Why: The earlier reasoning findings corrected current TOMLs, but the sync still ignores the route-specific reasoning fields documented in provider.toml. If a route later reports supports_reasoning = false, or selection falls back to a vendor without reasoning, existing.reasoning / existing.reasoning_options survive and the generated model continues advertising unsupported controls. A conservative fix is to emit reasoning = false and remove reasoning_options when the selected route explicitly reports false, while continuing to preserve curated options for supported routes whose exact values are not exposed. Add a regression test for an existing reasoning model whose selected route reports false.

  • [medium] [possible mistake] packages/core/src/sync/providers/merge-gateway.ts:261 - Check: Treat prompt_caching.mode = "none" as an explicit no-cache signal. Why: The previous cache-pricing discussion fixed preservation for fields the API omits, but mergeGatewayCachePricing ignores mode; with { mode: "none" } and no numeric fields, both ?? existing fallbacks retain curated cache prices. That publishes cache pricing for a route explicitly reporting no prompt caching. Return undefined read/write prices for mode = "none", and add a regression test distinct from the existing mode = "automatic" preservation test.

@github-actions

Copy link
Copy Markdown
Contributor

Action items

  • [high] [possible mistake] providers/merge-gateway/models/deepseek/deepseek-r1.toml:3 - Check: reasoning must reflect the Merge route's actual behavior, and per AGENTS.md the prescribed representation when a model reasons but the route exposes no caller control is reasoning = true + reasoning_options = [], not reasoning = false. Why: This PR flips reasoning to false for ~30 models whose base metadata and own descriptions identify them as reasoning models — DeepSeek R1, Claude Opus 4.6/4.7/4.8, Sonnet 4.6, Haiku 4.5, GPT-oss-120b/20b, Nemotron Nano 9B v2, Gemma 4, many Qwen3.x thinking/instruct models, GLM-5.2, and Fugu Ultra — all driven by supports_reasoning === false in buildMergeGatewayModel (packages/core/src/sync/providers/merge-gateway.ts). The gpt-oss-120b/20b leading comments say the routes leave "callers [with] no reasoning control," which is exactly the no-control case AGENTS.md maps to reasoning = true + reasoning_options = []; yet those files set reasoning = false. The PR body cites the Merge API docs but documents no per-route evidence that supports_reasoning = false means the route emits no reasoning output rather than merely lacking a toggle. If the field means "no toggle," every one of these entries is wrong. Action: Verify with Merge Gateway that supports_reasoning = false definitively means the route produces no reasoning. If it only means "no caller control," restore reasoning = true and reasoning_options = [] on these models; either way, add a per-route citation/comment (deepseek-r1.toml currently has none) documenting the evidence.

  • [medium] [possible mistake] providers/merge-gateway/models/zai/glm-5.2.toml:2 - Check: interleaved reasoning output must not be advertised when reasoning = false. Why: The final file sets reasoning = false (line 2) while retaining [interleaved] field = "reasoning_content" (line 5) — internally inconsistent, since a route that does not reason cannot emit interleaved reasoning content. The sync preserves interleaved from the existing file even when supports_reasoning = false disables reasoning, so this contradiction is introduced by the PR. Action: Either drop the [interleaved] block when the selected route disables reasoning (and extend the sync to clear it), or confirm and document that this route still emits interleaved reasoning_content despite supports_reasoning = false.

  • [low] [possible mistake] providers/merge-gateway/models/qwen/qwen-flash.toml:9 - Check: Curated cache prices need a verifiable source when the public API does not expose them. Why: sync.md states the public Merge API "does not currently expose the full CMS metadata" and that cache prices are "curated from Gateway's route-level CMS," yet this PR adds cache_read/cache_write values across many new and updated models (e.g., qwen-flash cache_read = 0.0044, with a non-standard ~20% read ratio). The cited API docs do not contain these prices, so the values cannot be independently reviewed. Action: Provide or point to a verifiable source for the curated cache prices, and confirm a re-sync with the configured MERGE_GATEWAY_API_KEY is idempotent for these values.

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.

4 participants