mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(daemon): user-configurable model alias / redirect for the media dispatcher (#1277) Tilmirs's use case in #1277: their Doubao access has moved from `doubao-seedream-3-0-t2i-250415` to `doubao-seedream-5-0`, but the project's registered catalog still emits the old id. Every call fails because the old name no longer resolves at Volcengine. Until now the only workaround was patching the source on every update. This adds a user-configurable alias layer that swaps the catalog id for whatever wire-name the provider expects, without changing the catalog itself. Two storage layers (env wins over disk, matching the rest of media-config): 1. **Environment variable** `OD_MEDIA_MODEL_ALIASES` carries a JSON map: `'{"doubao-seedream-3-0-t2i-250415":"doubao-seedream-5-0"}'`. Single var, portable across shells (Windows cmd.exe rejects hyphens in env-var names, so the per-id pattern lefarcen suggested wouldn't have worked on Windows). Malformed JSON is tolerated — falls through to the on-disk map rather than blowing up mid-generation. 2. **media-config.json** gains a top-level `aliases` field: ```json { "providers": { ... }, "aliases": { "doubao-seedream-3-0-t2i-250415": "doubao-seedream-5-0" } } ``` The Settings UI's existing PUT writes providers only, so the writeStored path now reads the existing aliases and preserves them on every write. Without that, a Settings save would silently wipe the user's aliases. The Settings UI surface for editing aliases is a separate follow-up; manual JSON edit and the env var are the v1 entry points. The resolution happens inside `startMediaGeneration` after the catalog lookup and surface validation have already accepted the registered id, so users still get the "unknown model" error if they request a catalog id that doesn't exist. The swap only changes what the provider receives on the wire (volcengine, openai, grok, fal, nanobanana etc. each pass `ctx.model` straight into their request body). Per-provider auto-output-name and the file-naming side use the function-level `model` parameter (the catalog id), so a `.png` named after `doubao-seedream-3-0-t2i-250415` keeps surfacing the registered id the agent / CLI asked for, not the wire-level alias. `providerNote` strings include the wire name so the user can see what was actually sent. Public API additions: - `resolveModelAlias(projectRoot, modelId)` -> the wire name (or the original if no alias matches). - `readAliasMap(projectRoot)` -> { effective, env, stored } for the future Settings UI's source-attribution display. Tests - 8 new cases in tests/media-config.test.ts (suite goes 14 -> 22): pass-through, stored map, env map, env-over-stored precedence, malformed-env fall-through, coercion of bad entries (null / number / nested object / empty string / blank key), readAliasMap source attribution, and a writeConfig regression that pins alias preservation on a Settings-style provider PUT. Validated - pnpm guard clean - pnpm --filter @open-design/daemon typecheck clean (both tsconfig.json and tsconfig.tests.json) - Media test suite (media-config + media-tasks-routes + media-tasks-persistence + media-nanobanana): 33/33 Pre-existing daemon test failures on Windows (symlinks, CODEX_BIN runtime resolution, MCP config, skills, server-paths) are unrelated to this change and reproduce on a clean main checkout. * fix(daemon): preserve catalog id for capability branches, surface aliases via /api/media/config (PR #1309 review) Lefarcen + codex P2 on PR #1309: the alias swap overwrote `ctx.model` globally, which silently disabled every renderer branch that keys behaviour off the catalog id. A user aliasing `dall-e-3 -> azure-dalle3-deployment` would have the wire name swapped correctly but `body.response_format = 'b64_json'` and `body.quality = 'hd'` would no longer be set, because the `ctx.model.startsWith('dall-e-')` / `ctx.model === 'dall-e-3'` checks now saw the alias. The same regression hit the gpt-image-* size selection, the gpt-4o-mini-tts instructions branch, and the openaiSizeFor() sizing function. MediaContext now carries both fields: - `model` — the registered catalog id (`dall-e-3`, `gpt-4o-mini-tts`, `doubao-seedream-3-0-t2i-250415`). All model-family capability branches read from here. - `wireModel` — the post-alias wire name. Every `body.model = `, every URL template, and every `providerNote` string reads from here so the user sees what was actually sent and the provider gets the alias. Renderers updated: openai image (body.model + providerNote + openaiSizeFor keeps catalog), openai speech (body.model + providerNote + gpt-4o-mini-tts instructions keeps catalog), volcengine video (body + note), volcengine image (body + note + openaiSizeFor keeps catalog), grok image (body + note), grok video (body + note), nanobanana (`credentials.model || ctx.wireModel || default` chain), minimax TTS, fishaudio TTS. The MINIMAX/FISHAUDIO hardcoded maps now sit BEHIND the user alias: explicit user alias wins over the project's legacy rebranding table, then the table wins over the catalog id fallback. Stub-fallback diagnostics (the SVG placeholder + stub providerNote string) keep the catalog id since those are debug surfaces, not provider calls. Lefarcen P3: the PR description claimed readAliasMap was the daemon-public API, but the /api/media/config route returned only readMaskedConfig (which had no aliases field). readMaskedConfig now returns `{ providers, aliases: { effective, env, stored } }` so the future Settings UI PR can consume the source-attributed map directly. The `aliases` field is always present (empty maps when nothing is configured) so the UI has a stable shape to read. Tests - New `media-alias-capability.test.ts` (2 jsdom cases) drives generateMedia end-to-end with a stubbed fetch and asserts on the request body. Pins the regression: aliased dall-e-3 still sends `response_format: 'b64_json'` + `quality: 'hd'`; aliased gpt-4o-mini-tts still attaches the instructions field from the voice prop. - `media-config.test.ts` grows by 2 cases (suite goes 22 -> 24): readMaskedConfig surfaces the alias map (both env and stored sources), and the empty-state shape for fresh installs. Validated - pnpm guard clean - pnpm --filter @open-design/daemon typecheck clean (both tsconfig.json and tsconfig.tests.json) - Media test suite (config + alias-capability + nanobanana + tasks-persistence + tasks-routes): 37/37 --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com>