Files
open-design/apps/daemon/tests
d5566d7627 feat(daemon): user-configurable model alias for the media dispatcher (#1277) (#1309)
* 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>
2026-05-14 14:58:39 +08:00
..