feat: add Synthetic as a fourth model router
From the PR description
Summary
Adds Synthetic - a subscription gateway for open-weights models (GLM-5.2, Kimi K3, Qwen3.6, gpt-oss-120b, Nemotron) on an OpenAI-compatible endpoint - as a fourth model router.
⚠️ Stacked on #340. The first commit here is #340's; review only the second (
feat: add Synthetic as a fourth model router). Please merge #340 first - this rebases ontomaincleanly afterwards. #340 generalized the router layer toROUTER_SLUGS, which is what makes this one small.
What's added
lib/llm/openrouter.ts-syntheticjoins theRouterProviderunion:https://api.synthetic.new/openai/v1, overridable viaSYNTHETIC_BASE_URL.lib/userApiKeys.ts+ migration20260821_01-syntheticAPI-key provider (SYNTHETIC_API_KEYenv or encrypted per-user storage), withschema.sqlkept in step for the drift check.routes/models.ts-GET /models/synthetic, filtered to tool-capable text models exactly as the OpenRouter and Vercel routes are.- Settings → API Keys gains a Synthetic field; Settings → Routers gains a Synthetic picker; both model pickers and the Word add-in gain a "Synthetic" group.
Two things that needed handling rather than reuse
1. Prices carry a currency symbol. Synthetic quotes "prompt": "$0.000001" where every other router sends a bare number. The client's cost formatter does Number(value) * 1_000_000, so the $ renders every Synthetic price as NaN. catalogPrice now strips a leading symbol, which is a no-op for the existing routers.
2. Ids come in two families, and the shared label helper mangled one. Synthetic publishes pinned hf:<vendor>/<model> ids and floating aliases syn:<size>:<capability>. modelDisplayName reads : as a variant separator (for Ollama tags like llama3:8b), so syn:large:text rendered as "Syn (Large:text)". Synthetic options get a dedicated syntheticModelDisplayName - mirrored in the add-in and pinned by the cross-package drift guard - giving:
| catalog id | label |
|---|---|
syn:large:text |
Large (text) |
syn:small:vision |
Small (vision) |
hf:openai/gpt-oss-120b |
GPT OSS 120B |
hf:moonshotai/Kimi-K3 |
Kimi K3 |
One judgement call worth a reviewer's eye: in Settings → Routers an alias is listed under the Hugging Face id it currently resolves to (syn:large:text → "zai-org/GLM-5.2", with the raw id on the line beneath), because that is the only field saying what actually answers behind a floating alias. The composer picker has only the id, so it shows "Large (text)". Happy to make both surfaces use the terse form if you'd rather they match exactly.
Validation
Verified the route's transform against the live catalog: all 10 published models pass the tool-capable text filter, and prices parse to the expected per-million figures ($0.10-$3.00/M input).
- backend:
tsc --noEmitclean;vitest run- 710 passed, 24 skipped (+11 over #340's 699). - frontend:
tsc --noEmitclean;eslint0 errors;vitest run- 469 passed (+3). - word-addin:
typecheck:appandtypecheck:e2eclean; catalog-parity and composer-availability drift guards extended to the new router.
Three frontend files (mikeApi, useSelectedModel, ChatInput.modelSelection) fail identically on unmodified main in my sandbox - Blob.text is not a function, window.localStorage undefined, a local jsdom/env issue rather than a code change. CI should run them.
Our analysis
Add Synthetic as fourth model router — read the full analysis →
Think the analysis missed something the PR description covers?
Commits in this PR (1)
| SHA | Subject | Author | Date | |
|---|---|---|---|---|
23b41c5a | feat: add OpenCode Go as a third model router | duncanmcqueen | 2026-08-20 | ↗ GitHub |
commit bodyOpenCode Go (opencode.ai's low-cost hosted gateway for open coding models
- GLM-5, Kimi K3, Qwen3.8 Max, MiniMax-M3, DeepSeek V4) joins OpenRouter
and the Vercel AI Gateway as a router, reusing the existing router
plumbing end to end rather than adding a parallel one:
- opencode-go/<model> ids stream through the shared router adapter
against https://opencode.ai/zen/go/v1, with OPENCODE_GO_BASE_URL as the
usual proxy/test override. OpenRouter's attribution headers and
reasoning payload stay OpenRouter-only.
- New opencode-go user API key (OPENCODE_API_KEY env or encrypted
per-user storage) with a Settings > API Keys field and a migration
widening the user_api_keys provider CHECK.
- GET /models/opencode-go returns the live catalog with the key kept
server-side; Settings > Routers gains an OpenCode Go picker, and the
saved selection gates requests through the same user_router_models
choke point that stops a hand-crafted request from spending the
operator's env key on an arbitrary gateway model.
- Both model pickers and the Word add-in gain an "OpenCode Go" group.
Router slugs, model-id prefixes and API-key provider names are one string
per router, so routerForModelId, the selection lookups and the profile
payload are now driven by ROUTER_SLUGS instead of a hard-coded pair. The
one place the routers genuinely differ is catalog-id shape: OpenCode Go
publishes bare model names ("glm-5"), so id validation is per-router.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| ||||
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-364.md from
inside the repo you want the changes in.