feat: add Synthetic as a fourth model router

⛔ closed · #364 · open-legal-products/mike ← duncanmcqueen/mike · opened 1mo ago by duncanmcqueen · closed 1mo ago · +1,619-223 across 45 files · ↗ on GitHub

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 onto main cleanly afterwards. #340 generalized the router layer to ROUTER_SLUGS, which is what makes this one small.

What's added

  • lib/llm/openrouter.ts - synthetic joins the RouterProvider union: https://api.synthetic.new/openai/v1, overridable via SYNTHETIC_BASE_URL.
  • lib/userApiKeys.ts + migration 20260821_01 - synthetic API-key provider (SYNTHETIC_API_KEY env or encrypted per-user storage), with schema.sql kept 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 --noEmit clean; vitest run - 710 passed, 24 skipped (+11 over #340's 699).
  • frontend: tsc --noEmit clean; eslint 0 errors; vitest run - 469 passed (+3).
  • word-addin: typecheck:app and typecheck:e2e clean; 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 body
OpenCode 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.

⬇ Download capture-pull-364.md