Skip to content

[Bug]: No user-level default media model exists — every image project is pinned to vela/gpt-image-2, and configured media providers can never become the default #7088

Description

@crazykid95

Before you submit

  • I searched existing issues and confirmed this is not a duplicate.
  • I replaced the example text with the real behavior, reproduction steps, expected result, and version from the app's About menu or od --version.

What happened?

Nothing I configure under Settings → Media providers changes which model actually generates images. Every image project is created pinned to vela/gpt-image-2 (OpenDesign Cloud), and there is no setting anywhere in the product that changes that. With BYOK keys configured and no Cloud subscription, image generation just fails.

I read this in the source rather than guessing from behavior. It is not one bug; it is six, and flipping the default: true flag would only paper over it.

1. The catalog default is a compiled-in Cloud model

apps/web/src/media/models.ts:329

{ id: 'vela/gpt-image-2', label: 'gpt-image-2 (Cloud)', hint: 'OpenDesign Cloud · managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'], default: true },

apps/web/src/media/models.ts:670

export const DEFAULT_IMAGE_MODEL = IMAGE_MODELS.find((m) => m.default)?.id ?? IMAGE_MODELS[0]!.id;

Every creation path seeds from this constant — NewProjectPanel.tsx:361, EntryView.tsx:466, home-hero/media-surfaces.ts:98,317 — and persists the result into projects.metadata_json.imageModel.

2. There is no user-level media default anywhere in the product

This is the core of the issue. The one control that looks like a default is not one.
apps/web/src/components/SettingsDialog.tsx:5696 renders "Image generation model" (settings.byokImageModel) only under two BYOK chat protocols:

{apiProtocol === 'senseaudio' || apiProtocol === 'aihubmix' ? ( <label className="field"> <span className="field-label">{t('settings.byokImageModel')}</span>

and the code comment at SettingsDialog.tsx:1154 states the scope explicitly:

// byokImageModel applies to the protocols that inject the daemon-side // generate_image tool (SenseAudio, AIHubMix) — flipping to another BYOK // tab shouldn't carry an image-model choice into, say, the OpenAI form.

It feeds the BYOK chat proxy's injected tool (apps/daemon/src/routes/chat.ts:1677, defaultImageModel). It is not a global media default and was never designed as one.

Meanwhile the Media providers section (SettingsDialog.tsx:7798–7960) offers only: provider chips, API key, Base URL, and a per-provider Model field. No default-model selector. So a user who pastes an OpenAI, Grok, Nano Banana, Volcengine, Fal, ImageRouter or Leonardo key has configured a provider that the app will never choose on its own.

3. byokMediaDefaults — the only media-preference payload — is sent on exactly one runtime

apps/web/src/components/ProjectView.tsx:7786

const daemonByokOpenCode = config.agentId === 'byok-opencode';

apps/web/src/components/ProjectView.tsx:7889

...(daemonByokOpenCode ? { byokMediaDefaults: byokMediaDefaultsForRun({ /* ... */ }), } : {}),

The second streamViaDaemon call site (ProjectView.tsx:8062) passes it unconditionally, but that whole branch is the BYOK-OpenCode path. So for every local CLI agent — Claude Code, Codex, Cursor, DeepSeek Harness, Gemini, Qwen, Copilot — and for amr, no media preference of any kind reaches the daemon.

4. mediaDefaultsForRuntime re-pins Cloud for amr

apps/daemon/src/prompts/system.ts:566

function mediaDefaultsForRuntime(agentId, defaults) { if (agentId !== 'amr') return defaults; return { ...defaults, imageModel: defaults?.imageModel?.trim() || 'vela/gpt-image-2', videoModel: defaults?.videoModel?.trim() || 'vela/doubao-seedance-2-0-260128', }; }

5. The per-provider Model field cannot select a provider — routing is fixed by the catalog id

apps/daemon/src/media/index.ts:578

} else if (def.provider === 'vela' && surface === 'image') { /* renderVelaImage */ } else if (def.provider === 'openai' && surface === 'image') { /* renderOpenAIImage */

providers..model from media-config.json is only consulted after routing has landed on that provider (index.ts:991, 1026, 1680: const wireModel = (credentials.model || ctx.wireModel).trim()). It is a wire-name override, not a selector.

6. The alias map is not an escape hatch either

I checked this so maintainers don't have to. apps/daemon/src/media/index.ts:504

const canonicalModel = def.id; // provider already fixed by def const wireModel = await resolveModelAlias(projectRoot, canonicalModel);

Alias resolution runs after def is looked up, so OD_MEDIA_MODEL_ALIASES / media-config.json.aliases can only rename the wire model within the provider already chosen. Aliasing vela/gpt-image-2 → gpt-image-2 still routes through renderVelaImage.

7. Separate, self-contained bug: two creation paths disagree about prompt-template models

NewProjectPanel.tsx:565 honors the template's declared model, with a comment explaining exactly why that matters:

// When the user picks a curated prompt template, propagate the template's // declared modelandaspectonto the actual project state. Without // this the user picks (e.g.) a HyperFrames template butvideoModel // stays on the default seedance — the agent then dispatches the wrong // model and the render path mismatches the prompt. function handleImagePromptTemplate(pick) { const m = pick?.summary.model; if (m && (IMAGE_MODELS.some((x) => x.id === m) || m.startsWith('aihubmix-'))) setImageModel(m);

But the home-entry path throws that away — EntryView.tsx:466:

if (kind === 'image') { return { kind, imageModel: DEFAULT_IMAGE_MODEL, imageAspect: '1:1' }; }

and home-hero/media-surfaces.ts:317 likewise seeds model: DEFAULT_IMAGE_MODEL.

This is precisely what produced my broken project. It was created from the home entry using the bundled "3D Stone Staircase Evolution Infographic" template, whose summary declares "model": "gpt-image-2" — yet the project was written with "imageModel": "vela/gpt-image-2". The stored metadata contains both values, contradicting each other (see Logs).

Two contributing factors
Near-identical labels. models.ts:329 is gpt-image-2 (Cloud) (provider vela); models.ts:336 is gpt-image-2 (provider openai). They sit adjacent in the picker. A user who has just pasted an OpenAI key reasonably reads the first as theirs — only the Cloud entries carry a provider suffix, the BYOK ones do not.

The Cloud default is offered without checking it is usable. With no vela login, renderVelaImage shells out to vela media models --json (apps/daemon/src/media/vela.ts:265) and the raw CLI string surfaces verbatim as a 400. vela is one of only two providers that rethrow instead of falling back (index.ts:779), and nothing verifies the Cloud route is authenticated before handing that model out as the default on a fresh install.

Steps to reproduce

  1. Install OpenDesign 0.19.2 on a clean profile. Do not run vela login (no OpenDesign Cloud subscription).
  2. Select any local CLI agent in the top bar — DeepSeek Harness, Claude Code, Codex, etc.
  3. Settings → Media providers → paste a valid OpenAI API key, leave Base URL at https://api.openai.com/v1, save. The provider shows Configured.
  4. From the home entry, pick the bundled image prompt template "3D Stone Staircase Evolution Infographic" (its summary declares model: gpt-image-2) and create the project.
  5. Enter a prompt and generate.
  6. Generation fails. media_tasks.model is vela/gpt-image-2 — the OpenAI key is never reached, and the template's declared model was discarded.
  7. Look for any way to make the configured provider the default for future projects. There is none: the Media providers section has no default selector, and "Image generation model" only appears under the SenseAudio / AIHubMix BYOK tabs.

Expected behavior

A configured media provider must be usable as the default — for every provider and every agent, not just OpenAI and not just byok-opencode. Concretely:

  1. Introduce a real user-level media default per surface (image / video / speech / sfx / music), editable in Settings → Media providers, offering any model whose provider is configured. This is the missing primitive; items 2–4 all depend on it.
  2. Demote DEFAULT_IMAGE_MODEL to a last-resort fallback, consulted only when the user has expressed no preference. All three creation paths (NewProjectPanel, EntryView, home-hero) should seed from the stored preference first.
  3. Send media defaults on every runtime. Rename byokMediaDefaults → mediaDefaults and drop the daemonByokOpenCode gate at ProjectView.tsx:7889. If the daemon can dispatch media for a local CLI agent, it must also receive that agent's media preferences.
  4. Stop overwriting an explicit preference in mediaDefaultsForRuntime. The amr branch should fill a Cloud default only when the user has chosen nothing.
  5. Fix the creation-path inconsistency (defect 7). EntryView.tsx:466 and home-hero/media-surfaces.ts:317 should propagate the prompt template's declared model/aspect exactly as NewProjectPanel.handleImagePromptTemplate already does. This one is independently shippable and low-risk.
  6. Gate Cloud models on an authenticated Vela profile. On a machine with no valid profile, no vela/* model should be selected as a default; show those entries as requiring sign-in, and surface a real message instead of passing through Error: saved profile is missing control key; run \vela login``.
  7. Disambiguate catalog labels — suffix the provider on every entry, so gpt-image-2 (OpenAI) sits next to gpt-image-2 (Cloud).
  8. Allow the model to be changed after creation, and/or migrate existing projects.metadata_json.imageModel, so users are not stuck with a value a hardcoded constant chose for them.

OpenDesign version

0.19.2

Platform

macOS (Apple Silicon)

Logs (optional)

Screenshots (optional)

No response

Additional context

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions