Skip to content

feat(web-shell): add daemon UI support for vision model selection - #6209

Merged
wenshao merged 9 commits into
QwenLM:mainfrom
AmariahAK:main
Jul 3, 2026
Merged

wenshao merged 9 commits into
QwenLM:mainfrom
AmariahAK:main

Conversation

@AmariahAK

Copy link
Copy Markdown
Contributor

What this PR does

Adds /model --vision support to the web-shell daemon UI, enabling users to select and persist a vision bridge model through the same surfaces already available for main, fast, and voice models. Running /model --vision opens the model picker in vision mode; running /model --vision <model-id> immediately persists the selection. The vision model also appears as a clickable sub-dialog button in the Settings page, and the /model command's argument hint now includes --vision.

Why it's needed

The CLI already supports /model --vision (added in #5597), but the daemon/web UI had no equivalent. Daemon users could configure main, fast, and voice models through the UI, but had to switch to the CLI or manually edit settings to set the vision bridge model. This PR closes that parity gap by following the exact same pattern already used for --fast and --voice.

Reviewer Test Plan

How to verify

  1. Start the daemon (npm run dev:daemon) and open the web UI
  2. Run /model --vision — confirm the model picker opens with title "Set Vision Model"
  3. Run /model --vision <model-id> — confirm the setting persists (check Settings page shows the value)
  4. Open Settings → find "Vision Model" — confirm it renders as a clickable button that opens the model picker in vision mode
  5. Switch UI language to Chinese — confirm "设置视觉模型" appears in the dialog title and aria-label
  6. Type /model in the composer — confirm the argument hint shows [--fast|--voice|--vision] [<model>]

Evidence (Before & After)

N/A (non-visual change; verified via unit tests — 698 tests pass, 0 failures)

Tested on

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

npm run dev:daemon (local), cd packages/web-shell && npx vitest run (unit tests)

Risk & Scope

  • Main risk or tradeoff: None — this is a pure extension of existing patterns (--fast/--voice) with no new logic or backend changes. The visionModel setting already has showInDialog: true in the schema.
  • Not validated / out of scope: Windows and Linux manual testing (CI covers these). The vision model validation logic (image-capability check, primary-model rejection, ambiguous resolution) remains in the CLI's modelCommand.ts and is not duplicated here — the daemon UI delegates persistence to setWorkspaceSetting, which the backend handles.
  • Breaking changes / migration notes: None.

Linked Issues

Closes #5597 (extends CLI vision support to daemon UI)

中文说明

为 web-shell 守护进程 UI 添加 /model --vision 支持,使用户能够通过已有的主模型、快速模型和语音模型相同的界面来选择和持久化视觉桥接模型。运行 /model --vision 会以视觉模式打开模型选择器;运行 /model --vision <model-id> 会立即持久化选择。视觉模型也会在设置页面中显示为可点击的子对话框按钮,且 /model 命令的参数提示现在包含 --vision。

CLI 已经支持 /model --vision(在 #5597 中添加),但守护进程/web UI 没有等效功能。守护进程用户可以通过 UI 配置主模型、快速模型和语音模型,但必须切换到 CLI 或手动编辑设置才能设置视觉桥接模型。本 PR 通过遵循与 --fast 和 --voice 完全相同的模式来关闭这个对等差距。

Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot @DragonnZhang kindly review the pr

atlarix-agent and others added 3 commits July 2, 2026 19:56
Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot the pr should be good now, kindly take a look

@AmariahAK

Copy link
Copy Markdown
Contributor Author

@wenshao kindly review

@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwencoder kindly review

@AmariahAK
AmariahAK dismissed a stale review via bd4c5b6 July 2, 2026 17:46
…t changes

These were local environment artifacts accidentally staged alongside the
/model --vision feature. Reverting to keep the PR focused.

- Restore .gitignore to base state
- Restore package-lock.json (fsevents peer flag, test-utils entry)
- Restore NOTICES.txt (hasown version)
Co-authored-by: atlarix-agent <agent@atlarix.dev>
Prevents Atlarix workspace files from being tracked in the repo.
Co-authored-by: atlarix-agent <agent@atlarix.dev>
@wenshao

wenshao commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

[reportError, setWorkspaceSetting, t],
);

const handleVisionModelSelect = useCallback(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Vision model stored as bare ID — core resolution expects provider-qualified encoding

Both this handler and the /model --vision <id> inline path (line 2433) call setWorkspaceSetting('workspace', 'visionModel', modelId) with a bare model ID. The CLI path uses encodeVisionModelSelector(selected) which produces authType:modelId\0baseUrl (see packages/cli/src/ui/components/ModelDialog.tsx:600).

Core's resolveVisionModelSelection() (packages/core/src/config/config.ts:3147) parses through parseVisionModelSetting, then calls resolveModelId(parsedSetting.selector) which expects the authType:modelId format. When it receives a bare ID, resolveModelId cannot extract an authType qualifier — if the same model ID appears on multiple providers/endpoints (e.g., gpt-4o on both OpenAI direct and Azure), the ambiguity guard at line 3187 silently drops the pin. The user sees a success toast but their choice is discarded.

The CLI also calls config?.setVisionModel(visionModel) after persisting to sync the runtime — the web-shell path skips this, so the pin doesn't take effect until the next Config rebuild.

Suggested change
const handleVisionModelSelect = useCallback(
const handleVisionModelSelect = useCallback(
(model: { id: string; authType?: string; baseUrl?: string }) => {
const encoded = model.authType
? `${model.authType}:${model.id}${model.baseUrl ? `\0${model.baseUrl}` : ''}`
: model.id;
setWorkspaceSetting('workspace', 'visionModel', encoded).catch(
(error: unknown) => reportError(error, t('model.setVision')),
);
},
[reportError, setWorkspaceSetting, t],
);

— qwen3.7-max via Qwen Code /review

@@ -3453,6 +3479,8 @@ export function App({
handleFastModelSelect(modelId);
} else if (modelDialogMode === 'voice') {
handleVoiceModelSelect(modelId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Missing currentVisionModel — dialog highlights wrong model

The currentModelId prop only handles 'voice' mode (currentVoiceModel). For vision mode it falls through to undefined, and ModelDialog falls back to connection.currentModel (the main chat model). The user can't see which vision model is active in the picker.

Derive currentVisionModel from workspaceSettings the same way currentVoiceModel is computed (~line 1649):

const currentVisionModel = (() => {
  const value = workspaceSettings.find(
    (setting) => setting.key === 'visionModel',
  )?.values.effective;
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
})();

Then extend the prop:

currentModelId={
  modelDialogMode === 'voice' ? currentVoiceModel
    : modelDialogMode === 'vision' ? currentVisionModel
    : undefined
}

— qwen3.7-max via Qwen Code /review

? t('model.setVision')
: t('model.select')
}
size="lg"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Dialog title ternary duplicated + onSelect handler not exhaustive

This 4-way ternary (main → fast → voice → vision → default) is duplicated in ModelDialog.tsx:175-179 for the listbox aria-label. Adding a 5th mode requires updating both files in lockstep — forgetting one causes visible/accessible title disagreement.

Separately, the onSelect dispatch at line 3485 uses if/else if/else with no exhaustive check. A new ModelDialogMode member compiles cleanly even without a handler.

Consider extracting shared lookups:

const MODE_TITLE_KEY: Record<ModelDialogMode, string> = {
  main: 'model.select',
  fast: 'model.setFast',
  voice: 'model.setVoice',
  vision: 'model.setVision',
};

// In onSelect:
const handlers: Record<ModelDialogMode, (id: string) => void> = {
  main: handleModelSelect,
  fast: handleFastModelSelect,
  voice: handleVoiceModelSelect,
  vision: handleVisionModelSelect,
};
handlers[modelDialogMode ?? 'main'](modelId);

— qwen3.7-max via Qwen Code /review

'model.select': '选择模型',
'model.setFast': '设置 Fast Model',
'model.setVoice': '设置语音模型',
'model.setVision': '设置视觉模型',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Missing settings.label.visionModel / settings.description.visionModel i18n keys

The visionModel setting is now exposed in the Settings dialog via SUB_DIALOG_KEYS (SettingsMessage.tsx), but there are no settings.label.visionModel or settings.description.visionModel entries in either locale. The fastModel setting has both (settings.label.fastModel: '快速模型', etc.), so Chinese users will see the daemon-provided English fallback for the vision model row while fast model is properly localized.

Add to both EN and ZH, mirroring the fastModel pattern:

// EN
'settings.label.visionModel': 'Vision Model',
'settings.description.visionModel': 'Image-capable model used as the vision bridge. Leave empty to auto-select.',

// ZH
'settings.label.visionModel': '视觉模型',
'settings.description.visionModel': '用于视觉桥接的图像能力模型。留空则自动选择。',

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — real build + real-UI E2E

Verified PR #6209 at head ac7f851 on a clean worktree off current origin/main. Net two‑dot diff is exactly the 6 files the PR advertises (+40 / −5). Everything was driven against a real qwen serve --web daemon serving the PR's own built bundle (index-DH94VN1M.js, confirmed to contain the new i18n strings) using headless Chromium (Playwright).

Results

Check Result
npm run build (incl. tsc -p tsconfig.lib.json typecheck) ✅ exit 0
web-shell unit tests (vitest) ✅ 698 / 698 pass, 55 files (matches PR claim)
tsc web-shell + eslint on the 5 changed files ✅ clean
Real-UI E2E (Chromium vs live daemon) ✅ 9 / 9 behavioral checks
Mutation A/B (revert 5 files → rebuild → restore) ✅ vision behavior is load-bearing

Real-UI E2E covered: /model --vision opens the picker titled “Set Vision Model”; /model --vision <id> persists (workspace .qwen/settings.json written); Settings → Vision Model renders as a clickable button showing the persisted value and opening the picker in vision mode; picker selection persists; Chinese UI shows “设置视觉模型”; the /model argument hint shows [--fast|--voice|--vision] [<model>]. I also confirmed the PR-body claim that visionModel already has label:'Vision Model' + showInDialog:true in settingsSchema.ts.

Mutation A/B (reverting the 5 web-shell files to origin/main, rebuilding, then restoring):

A (PR)          -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}
B (base revert) -> {"en_picker":false, "zh_picker":false, "arghint_has_vision":false}
A (restored)    -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}

Screenshots

/model --vision (EN) Settings → Vision Model button
en picker settings
/model --vision (ZH “设置视觉模型”) /model argument hint
zh picker arg hint

⚠️ One functional gap worth addressing: picker selection isn't honored by the vision bridge

The UI is correct, but the value the picker persists is a format the vision bridge can't resolve, so the user's explicit choice is silently dropped to same‑provider auto‑select (a debug-level warn, no visible error). The Settings page still shows the value, which masks it.

Root cause. Client model ids are ACP-encoded modelId(authType) (formatAcpModelId unconditionally returns `${modelId}(${authType})`). handleVisionModelSelect / the picker persist that raw id into the visionModel setting with no encoding. Core reads it back through Config.resolveVisionModelSelection() → resolveModelId(), which only understands authType:modelId (colon) or a bare id — not the (authType) suffix — so it fails to match any getAllConfiguredModels() entry (whose ids are bare) and returns undefined → auto-select.

Reproduced with the actual exported core functions against the exact value the picker wrote to disk ({"visionModel":"coder-model(qwen-oauth)"}):

"coder-model(qwen-oauth)"  (web-shell PICKER)  -> resolveModelId -> {modelId:"coder-model(qwen-oauth)"}  -> matched: FALSE (0)
"qwen-oauth:coder-model"   (CLI encodeVisionModelSelector) -> {authType,modelId}                        -> matched: TRUE
"coder-model"              (bare id)                        -> {modelId:"coder-model"}                    -> matched: TRUE

Every other model-selection path avoids this: the main-model picker routes the ACP id through parseAcpModelOption (which strips the (authType) suffix), and the CLI's vision picker encodes to authType:modelId via encodeVisionModelSelector. The web-shell vision picker does neither.

  • Works today: /model --vision <bare-id> (direct command with a plain id, as in the test plan) — persists verbatim and resolves. ✅
  • Silently no-ops: selecting a model from the picker / Settings button — persists modelId(authType), which never resolves. ⚠️

Suggested fix (small, client-only): encode the vision picker selection before persisting — strip the ACP (authType) suffix and store authType:modelId (mirror encodeVisionModelSelector), e.g. in handleVisionModelSelect. A round-trip unit test (picker id → setting → resolveVisionModelSelection matches) would lock it in.

Minor / non-blocking

  • .gitignore adds .atlarix/ — unrelated to the feature; recommend dropping it to keep the diff scoped.
  • Vision picker doesn't pre-highlight the current vision model (currentModelId={undefined} → it highlights the main model). Consistent with the --fast pattern; only --voice passes a current value. Minor polish.
  • Vision picker isn't filtered to image-capable models (unlike --voice, and unlike the CLI which warns on a non-image pick). The PR calls this out as out-of-scope; combined with the resolver gap above, a user currently gets no feedback if their pick won't take effect.

Recommendation

UI parity is solid and fully verified — this is a clean, well-scoped extension of the --fast/--voice pattern. I'd recommend landing the small picker-encoding fix (so the picker/Settings button actually pin the model, not just display it) either in this PR or a tracked fast-follow; the direct /model --vision <id> command already works.

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 — 真实构建 + 真实 UI 端到端

在基于当前 origin/main 的干净 worktree 上验证了 PR #6209(head ac7f851)。两点净 diff 正好是 PR 声明的 6 个文件(+40 / −5)。所有验证都针对真实的 qwen serve --web 守护进程、服务 PR 自己构建的 bundle(index-DH94VN1M.js,已确认含新增 i18n 字符串),用无头 Chromium(Playwright)驱动。

结果

检查项 结果
npm run build(含 tsc -p tsconfig.lib.json 类型检查) ✅ 退出 0
web-shell 单元测试(vitest) ✅ 698 / 698 通过,55 个文件(与 PR 声称一致)
web-shell tsc + 对 5 个改动文件 eslint ✅ 干净
真实 UI 端到端(Chromium 对真实守护进程) ✅ 9 / 9 行为检查
变异 A/B(回退 5 文件 → 重建 → 还原) ✅ vision 行为承重

真实 UI 端到端覆盖: /model --vision 打开标题为 “Set Vision Model” 的选择器;/model --vision <id> 持久化(写入 workspace .qwen/settings.json);设置 → Vision Model 渲染为可点击按钮,显示已持久化的值并以 vision 模式打开选择器;选择器选择可持久化;中文界面显示 “设置视觉模型”;/model 参数提示显示 [--fast|--voice|--vision] [<model>]。我还核实了 PR 描述中的说法——visionModel 在 settingsSchema.ts 中已有 label:'Vision Model' + showInDialog:true。

变异 A/B(把 5 个 web-shell 文件回退到 origin/main、重建、再还原):

A (PR)      -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}
B (回退基线) -> {"en_picker":false, "zh_picker":false, "arghint_has_vision":false}
A (还原)     -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}

截图

见上方英文版四张图:/model --vision(英文)、设置页 Vision Model 按钮、/model --vision(中文“设置视觉模型”)、/model 参数提示。


⚠️ 一个值得处理的功能缺口:选择器所选模型未被视觉桥接实际采用

UI 是正确的,但选择器持久化的值是视觉桥接无法解析的格式,因此用户的显式选择会被静默降级为同 provider 的 auto-select(仅 debug 级警告,无可见错误)。设置页仍显示该值,从而掩盖了问题。

根因。 客户端模型 id 是 ACP 编码的 modelId(authType)(formatAcpModelId 无条件返回 `${modelId}(${authType})`)。handleVisionModelSelect / 选择器把这个原始 id 未经编码写入 visionModel 设置。core 通过 Config.resolveVisionModelSelection() → resolveModelId() 读回,而后者只认 authType:modelId(冒号)或裸 id——不认 (authType) 后缀——所以匹配不到任何 getAllConfiguredModels() 条目(其 id 是裸的),返回 undefined → auto-select。

用真实导出的 core 函数针对选择器实际写入磁盘的值({"visionModel":"coder-model(qwen-oauth)"})复现:

"coder-model(qwen-oauth)"  (web-shell 选择器) -> resolveModelId -> {modelId:"coder-model(qwen-oauth)"} -> 匹配: 否 (0)
"qwen-oauth:coder-model"   (CLI encodeVisionModelSelector)        -> {authType,modelId}                -> 匹配: 是
"coder-model"              (裸 id)                                -> {modelId:"coder-model"}            -> 匹配: 是

其他所有模型选择路径都规避了这一点:主模型选择器把 ACP id 走 parseAcpModelOption(会剥离 (authType) 后缀),CLI 的 vision 选择器用 encodeVisionModelSelector 编码成 authType:modelId。web-shell 的 vision 选择器两者都没做。

  • 当前可用: /model --vision <裸 id>(直接命令输入普通 id,如测试计划所示)——原样持久化并能解析。✅
  • 静默失效: 从选择器 / 设置按钮选择模型——持久化 modelId(authType),永远解析不了。⚠️

建议修复(很小,仅客户端): 在持久化前对 vision 选择器的选择做编码——剥离 ACP (authType) 后缀并存 authType:modelId(比照 encodeVisionModelSelector),例如在 handleVisionModelSelect 中。再补一个往返单测(选择器 id → 设置 → resolveVisionModelSelection 能匹配)以固化。

次要 / 非阻塞

  • .gitignore 增加 .atlarix/ — 与本功能无关;建议移除以保持 diff 聚焦。
  • Vision 选择器不预选当前视觉模型(currentModelId={undefined} → 高亮的是主模型)。与 --fast 模式一致;只有 --voice 传了当前值。属细节打磨。
  • Vision 选择器未按图像能力过滤(不同于 --voice,也不同于 CLI 对非图像选择会警告)。PR 已声明此为 out-of-scope;结合上面的解析缺口,用户目前在选到无法生效的模型时得不到任何反馈。

结论

UI 对等做得扎实、已完整验证——是对 --fast/--voice 模式干净且聚焦的扩展。建议把上面那个很小的“选择器编码”修复(让选择器/设置按钮真正 pin 住模型,而不仅仅显示)纳入本 PR 或一个有跟踪的快速跟进;直接命令 /model --vision <id> 目前已可用。

Verified locally on macOS: worktree at ac7f851, real qwen serve --web daemon + Playwright/Chromium against the PR's own build; mutation A/B + core-function repro for the resolver gap.

@wenshao
wenshao added this pull request to the merge queue Jul 3, 2026
Merged via the queue into QwenLM:main with commit ea0749e Jul 3, 2026
49 checks passed
zhangxy-zju pushed a commit to zhangxy-zju/qwen-code that referenced this pull request Jul 3, 2026
QwenLM#6236)

* fix(web-shell): encode vision model selection & polish picker

Address Wenshao's review comments on QwenLM#6209:

[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
  as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
  model ID appears on multiple providers

[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
  vision model instead of falling back to the main model

[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
  Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
  would fail at compile time if a new mode is added without a handler

[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
  in the Settings dialog

Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)

* fix(web-shell): encode vision model selection & polish picker

Address Wenshao's review comments on QwenLM#6209:

[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
  as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
  model ID appears on multiple providers

[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
  vision model instead of falling back to the main model

[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
  Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
  would fail at compile time if a new mode is added without a handler

[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
  in the Settings dialog

Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)

* fix(web-shell): address review comments for vision model picker encoding

- Extract encodeVisionModelForSetting / decodeVisionModelForPicker into
  shared utils/modelEncoding.ts so they can be tested in isolation
- Add 17 unit tests covering ACP encoding, colon-bearing IDs, empty
  parens passthrough, and round-trip identity
- Memoize modelHandlers record with useMemo to avoid re-allocation on
  every model picker click
- Replace dead fallback (?? 'main') — the outer modelDialogMode guard
  already ensures non-null, so use an explicit if-guard instead

* test(web-shell): add edge-case tests for model encoding functions

- Add passthrough tests for already-encoded colon format
- Add malformed input tests (bare authType, unclosed paren, double-parens)
- Add leadin-colon malformed input test for decode
- Add empty string passthrough test
- 23 encoding tests passing (up from 17), full suite: 735 passing

* fix: PR QwenLM#6236 follow-up — vision model encoding + fast model highlight

- decodeVisionModelForPicker: strip \0baseUrl suffix before decoding to ACP
- Remove dead encodeFastModelForSetting (fast picker strips ACP suffix before handler)
- Add currentFastModel derivation + 'fast' branch to currentModelId ternary
- Fix misleading voice handler comment (bare IDs, not ACP)
- Replace unnecessary useMemo on modelHandlers with plain object

Co-authored-by: atlarix-agent <agent@atlarix.dev>

---------

Co-authored-by: Qwen3.6 Plus agent <agent@atlarix.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add /model --vision for fallback vision model

3 participants