Model router (meta-LLM profiles)
F12 · 25 mapped behaviors in the Self-Healing Codebase Project. Follow the prerequisites, actions and expected results; expand the original CLI recipe to replay it.
778 behavior IDs · 28 families · 844 named recipes & support steps
16 October 2026 · PR #18334 at 9a3c3a69
Canvas 1.27.1 · Agent Server 1.55.0 · automation 1.20.0
This is the map at the revision above, including its open bug notes. It does not incorporate later changes on main or claim a fresh verification run. Setup & reading guide · Download the scenario inventory
All scenarios & marker key · Open a script to read Given / Do / Expect. Expand the exact recipe for replayable commands.
No scenarios match. Try a different word or reset.
F12 Model router (meta-LLM profiles)
25 behaviors · 28 scripts & contracts · Maintained source
Entry points & prerequisites
What this family covers
A model router (the UI also calls it a "meta-profile") names a classifier LLM profile, a prompt template and a model table. While a router is active, every new conversation gets a route_task_to_model tool: the classifier picks one saved LLM profile from the table and the agent switches to it (the chat shows Switched to profile <name>). With Run on first message on, the agent is told to route the first user message before doing anything else. Users create routers from the Router Pro / Router Flash templates or from scratch, can have the missing router LLM profiles created from a provider connection, and activate, edit and delete routers under Settings → Model Router.
How a user gets here
- Sidebar settings gear (
backend-selector-settings-link), then Model Router in the settings navigation (sidebar-settings-/settings/meta-llm, between LLM and Condenser). Used byF12.page. - Direct URL
/settings/meta-llm. Used by most recipes (browser goto). - Command menu (
Control+korMeta+k, orcommand-menu-triggerin the sidebar): search "Model Router" (or "router", "meta"), choose Model Router ("Meta-profiles that route each task to the right LLM profile."). Used byF12.page. - Per-router actions: the row
...button (meta-profile-menu-trigger-<name>), then Edit / Set active / Delete. - The effect of a router is seen in a conversation started from the home composer (
F12.route-first-message; see F05/F06 for the composer and activity rendering).
Given
Preconditions:
F12.empty-statesneeds a freshlaunch --newwithonboard --skipdone and beforellm preset deepseek(no LLM profiles, no routers).- Everything after it needs
control-openhands llm preset deepseek --api-key-file <key file>(profilesdeepseek-flashanddeepseek-pro), no meta-profile namedQA_*(control-openhands api GET /api/meta-profiles), no provider connection (control-openhands api GET /api/llm/provider-connectionsis[]) and no LLM profile nameddeepseek-chat(control-openhands api GET /api/profiles). F12.add-connectionandF12.route-first-messageuse the DeepSeek key file; never print it.- Blocked here:
F12.unsupported(needs an agent server older than software-agent-sdk #3744, for examplelaunch --new --sdk-ref <older release>),F12.cloud(needs a Cloud account, with and without an organization) andF12.load-error(needs/api/meta-profilesto fail while the rest of the agent server answers;service stop agent-serverswaps the whole app for the Manage backends card instead). - Desktop viewport unless a bullet says otherwise.
Empty statesF12.empty-statesMapped behavior
Scenario F12--empty-states · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /settings/meta-llm.
Expect
with no LLM profiles the page says Create LLM profiles first — meta-profiles route between your saved LLM profiles.; with no routers it says No meta-profiles yet. Add one to route tasks across your LLM profiles. and Run on first message is off and disabled.
Exact CLI recipe & bug checks
On the fresh run run control-openhands browser goto /settings/meta-llm, control-openhands browser text 'testid=meta-profile-no-llm-profiles', control-openhands browser text 'testid=meta-profile-empty' and control-openhands browser eval "(()=>{const i=document.querySelector('[data-testid=meta-profile-run-at-conversation-start-switch]');return {checked:i.checked,disabled:i.disabled}})()". The texts are the two messages in Sub-features and the switch is {checked:false, disabled:true}. control-openhands browser screenshot --feature F12.empty-states --name no-llm-profiles shows both messages above a greyed switch.
Template chooserF12.template-chooserMapped behavior
Scenario F12--template-chooser · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template cancel”.
- DoPress Escape.
- DoChoose “Meta profile template router pro”.
- DoChoose “Meta profile cancel”.
- DoUse browser mouse click as specified in the exact recipe.
Expect
Add Model Router opens Choose Model Router template (Add OpenHands Router Pro, Add OpenHands Router Flash, Custom, Cancel); Cancel, Escape and a backdrop click close it; Pro and Flash prefill the name, classifier minimax-m3, their own long routing prompt and the same 11-model table.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile' and control-openhands browser snapshot 'role=dialog': dialog Choose Model Router template with buttons Add OpenHands Router Pro, Add OpenHands Router Flash, Custom, Cancel. control-openhands browser click 'testid=meta-profile-template-cancel' then control-openhands browser count 'testid=meta-profile-template-modal' is 0; reopen and control-openhands browser press Escape gives 0 too. Reopen, control-openhands browser click 'testid=meta-profile-template-router-pro', then control-openhands browser value 'testid=meta-profile-name-input' (openhands-router-pro), control-openhands browser value 'testid=meta-profile-classifier-input' (minimax-m3), control-openhands browser eval "document.querySelector('[data-testid=meta-profile-model-table]').value.split('\n').length" (11), control-openhands browser eval "document.querySelector('[data-testid=meta-profile-prompt-template]').value.includes('{{ instance_text }}')" (true), control-openhands browser value 'testid=meta-profile-router-connection' (Don't create profiles while no connection exists) and control-openhands browser enabled 'testid=meta-profile-save' (true). Leave with control-openhands browser click 'testid=meta-profile-cancel'. The Flash template (testid=meta-profile-template-router-flash) gives openhands-router-flash, the same classifier and identical table, and a different prompt (about 5,900 characters against Pro's 5,000; compare with browser eval "document.querySelector('[data-testid=meta-profile-prompt-template]').value.length"). A backdrop click also closes the chooser: reopen it and control-openhands browser mouse-click 20 980, then count is 0.
Custom templateF12.template-customMapped behavior
Scenario F12--custom-template · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template custom”.
Expect
Custom opens a blank editor; the placeholders show an example prompt and a two-line example model table.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', then control-openhands browser value on testid=meta-profile-name-input, testid=meta-profile-classifier-input, testid=meta-profile-prompt-template and testid=meta-profile-model-table (all ""), control-openhands browser enabled 'testid=meta-profile-save' (false) and control-openhands browser eval "document.querySelector('[data-testid=meta-profile-model-table]').placeholder.split('\n')": two example rows, ["- GPT-5.4: strong for coding and reasoning", "- MiniMax-M3: efficient fallback"], joined by a real line break (#18035), and control-openhands browser screenshot --feature F12.template-custom --name custom-empty shows them on two lines under the prompt placeholder, which breaks lines the same way.
ValidationF12.editor-validationMapped behavior
Scenario F12--validation · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “QA router!” in “Meta profile name”.
- DoEnter “qa-classifier” in “Meta profile classifier”.
- DoEnter “Pick a model for: {{ instance_text }}” in “Meta profile prompt template”.
- DoEnter “QA_router” in “Meta profile name”.
- DoEnter “Pick a model for: {{ task }}” in “Meta profile prompt template”.
- DoEnter “” in “Meta profile classifier”.
Expect
Save stays disabled until the name is valid, a classifier is set and the prompt contains {{ instance_text }} (spaces inside the braces optional).
Exact CLI recipe & bug checks
In that Custom editor run control-openhands browser fill 'testid=meta-profile-name-input' 'QA router!', control-openhands browser fill 'testid=meta-profile-classifier-input' qa-classifier, control-openhands browser fill 'testid=meta-profile-prompt-template' 'Pick a model for: {{ instance_text }}' and control-openhands browser enabled 'testid=meta-profile-save' (false: bad name). control-openhands browser fill 'testid=meta-profile-name-input' QA_router turns it true; control-openhands browser fill 'testid=meta-profile-prompt-template' 'Pick a model for: {{ task }}' turns it false; 'Pick a model for: {{instance_text}}' turns it true again; control-openhands browser fill 'testid=meta-profile-classifier-input' '' turns it false. Keep the editor open.
Add a connectionF12.add-connectionMapped behavior
Scenario F12--add-a-connection · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Meta profile add provider connection”.
- DoEnter “QA_deepseek” in “Provider connection name”.
- DoEnter “deepseek” in “Provider connection provider”.
- DoChoose “deepseek”.
- DoEnter “the value specified in the recipe” in “Provider connection api key”.
- DoChoose “Provider connection submit”.
- WaitWait for “Provider connection modal” to disappear.
Expect
Add connection in the create editor opens Add provider connection; saving it selects the new connection in the picker.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=meta-profile-add-provider-connection', control-openhands browser fill 'testid=provider-connection-name-input' QA_deepseek, control-openhands browser fill 'testid=provider-connection-provider-input' deepseek, control-openhands browser click 'role=option[name="deepseek"]', control-openhands browser fill 'testid=provider-connection-api-key-input' --value-file <key file>, control-openhands browser click 'testid=provider-connection-submit' and control-openhands browser wait 'testid=provider-connection-modal' --state detached. control-openhands browser toasts has Connection "QA_deepseek" created and control-openhands browser value 'testid=meta-profile-router-connection' is QA_deepseek (deepseek).
Create the first routerF12.create-first · F12.router-profilesKnown issue
Scenario F12--create-the-first-router · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Meta profile classifier”.
- DoChoose “deepseek-flash”.
- DoEnter “QA_router” in “Meta profile name”.
- DoEnter “the value specified in the recipe” in “Meta profile prompt template”.
- DoEnter “the value specified in the recipe” in “Meta profile model table”.
- DoChoose “Meta profile save”.
- WaitWait for the element identified in the CLI recipe to appear.
- DoReload the page.
Expect
saving the first router lists it, makes it Active and turns Run on first message on.
with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.
Linked known issues
Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).
Exact CLI recipe & bug checks
Write the prompt and table to files: printf 'You route tasks. Reply only with JSON {"model": "<name>", "reason": "<short>"} using one name from this table:\n{{ model_table }}\n\nTask:\n{{ instance_text }}' > <prompt file> and printf -- '- deepseek-flash: cheap, for simple tasks\n- deepseek-pro: stronger, for hard tasks\n- deepseek-chat: general chat' > <table file>. Run control-openhands browser click 'testid=meta-profile-classifier-input', control-openhands browser click 'role=option[name="deepseek-flash"]', control-openhands browser fill 'testid=meta-profile-name-input' QA_router, control-openhands browser fill 'testid=meta-profile-prompt-template' --value-file <prompt file>, control-openhands browser fill 'testid=meta-profile-model-table' --value-file <table file>, control-openhands browser click 'testid=meta-profile-save' and control-openhands browser wait-text 'Meta-profile "QA_router" saved'. After control-openhands browser reload: control-openhands browser text 'testid=meta-profile-row-QA_router' is QA_router\ndeepseek-flash · Direct prompt\nActive, the switch eval above returns {checked:true, disabled:false}, control-openhands api GET /api/meta-profiles has active_meta_profile QA_router, control-openhands api GET /api/settings has "run_router_at_conversation_start": true, and control-openhands api GET /api/profiles has a new deepseek-chat (deepseek/deepseek-chat, provider_connection_id set) while deepseek-flash/deepseek-pro are unchanged. control-openhands browser screenshot --feature F12.create-first --name list-after-create shows the row with the gold Active badge and the switch on.
Duplicate nameF12.duplicate-nameMapped behavior
Scenario F12--duplicate-name · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template custom”.
- DoEnter “QA_router” in “Meta profile name”.
- DoEnter “QA_router_2” in “Meta profile name”.
Expect
a new router cannot take an existing router's name (A meta-profile with this name already exists..., Save disabled).
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', control-openhands browser fill 'testid=meta-profile-name-input' QA_router, control-openhands browser text 'testid=meta-profile-name-taken' (A meta-profile with this name already exists. Choose a different name, or edit the existing one.), pick classifier deepseek-pro as above, fill the prompt with 'Route: {{ instance_text }}', and control-openhands browser enabled 'testid=meta-profile-save' (false). control-openhands browser fill 'testid=meta-profile-name-input' QA_router_2 makes control-openhands browser count 'testid=meta-profile-name-taken' 0 and Save true. Click testid=meta-profile-cancel; control-openhands browser count 'testid=meta-profile-row-QA_router_2' is 0.
"Don't create profiles" in a templateF12.router-profilesMapped behavior
Scenario F12--don-t-create-profiles-in-a-template · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template router pro”.
- DoChoose “Meta profile router connection”.
- DoChoose “Don't create profiles”.
- DoChoose “Meta profile cancel”.
Expect
with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-router-pro', control-openhands browser value 'testid=meta-profile-router-connection' (QA_deepseek (deepseek), preselected), control-openhands browser click 'testid=meta-profile-router-connection' and control-openhands browser click "role=option[name=\"Don't create profiles\"]" --observe 'testid=meta-profile-router-connection' --observe-ms 1500. The picker stays on Don't create profiles: the observed states are QA_deepseek (deepseek) at 0 ms and Don't create profiles about 300 ms later, with nothing after it, and control-openhands browser value 'testid=meta-profile-router-connection' is Don't create profiles. Cancel the editor (control-openhands browser click 'testid=meta-profile-cancel'); F12.create-more saves a template with this choice. The Custom template starts on Don't create profiles even with a connection present; there, pick role=option[name="QA_deepseek (deepseek)"] first and then Don't create profiles: the second pick sticks. Cancel the editor.
Create another router without new profilesF12.create-more · F12.router-profilesMapped behavior
Scenario F12--create-another-router-without-new-profiles · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template router flash”.
- DoEnter “QA_router_flash” in “Meta profile name”.
- DoChoose “Meta profile router connection”.
- DoChoose “Don't create profiles”.
- DoChoose “Meta profile save”.
- WaitWait for the element identified in the CLI recipe to appear.
- DoReload the page.
Expect
saving another router lists it and leaves the Active badge where it was.
with a connection picked under Create missing router LLM profiles using, Save creates an LLM profile <provider>/<name> linked to that connection for every model-table name (and the classifier) that does not exist yet; Don't create profiles creates none.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-router-flash', control-openhands browser value 'testid=meta-profile-router-connection' (QA_deepseek (deepseek), preselected), control-openhands browser fill 'testid=meta-profile-name-input' QA_router_flash, control-openhands browser click 'testid=meta-profile-router-connection', control-openhands browser click "role=option[name=\"Don't create profiles\"]" and control-openhands browser value 'testid=meta-profile-router-connection' (Don't create profiles). Then control-openhands browser click 'testid=meta-profile-save' and control-openhands browser wait-text 'Meta-profile "QA_router_flash" saved'. After control-openhands browser reload, control-openhands browser text 'testid=meta-profile-list' is QA_router\ndeepseek-flash · Direct prompt\nActive\nQA_router_flash\nminimax-m3 · Direct prompt and control-openhands browser count 'testid=meta-profile-active-badge' is 1. control-openhands api GET /api/profiles still lists only deepseek-chat, deepseek-flash and deepseek-pro: none of the template's 11 models became a profile, so QA_router_flash names a classifier (minimax-m3) that does not exist, which saving allows (see Gotchas).
EditF12.editMapped behavior
Scenario F12--edit · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Meta profile menu trigger qa router”.
- DoChoose “Meta profile actions menu” / “Meta profile edit”.
- WaitWait for the element identified in the CLI recipe to appear.
Expect
Edit opens Edit meta-profile with the name locked and no connection picker; changes persist.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router', control-openhands browser click 'testid=meta-profile-actions-menu >> testid=meta-profile-edit', control-openhands browser text 'testid=meta-profile-editor >> role=heading' (Edit meta-profile), control-openhands browser enabled 'testid=meta-profile-name-input' (false), control-openhands browser count 'testid=meta-profile-router-connection' (0). Pick classifier deepseek-pro, fill testid=meta-profile-model-table with the first two table lines only, click testid=meta-profile-save and control-openhands browser wait-text 'Meta-profile "QA_router" saved'. After a reload the row reads QA_router\ndeepseek-pro · Direct prompt, the badge stays on QA_router_flash, and control-openhands api GET /api/meta-profiles/QA_router shows the new classifier and table.
Cancel the connection dialogF12.add-connection-cancelMapped behavior
Scenario F12--cancel-the-connection-dialog · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Add meta profile”.
- DoChoose “Meta profile template custom”.
- DoEnter “QA_router_cancel” in “Meta profile name”.
- DoChoose “Meta profile add provider connection”.
- DoEnter “QA_unused” in “Provider connection name”.
- DoChoose the dialog / “Cancel”.
- WaitWait for “Provider connection modal” to disappear.
Expect
Cancel in Add provider connection closes it without creating a connection and keeps the editor's values and picker choice.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=add-meta-profile', control-openhands browser click 'testid=meta-profile-template-custom', control-openhands browser fill 'testid=meta-profile-name-input' QA_router_cancel, control-openhands browser click 'testid=meta-profile-add-provider-connection', control-openhands browser fill 'testid=provider-connection-name-input' QA_unused, control-openhands browser click 'role=dialog >> role=button[name="Cancel"]' (the Cancel button has no test id) and control-openhands browser wait 'testid=provider-connection-modal' --state detached. browser value 'testid=meta-profile-name-input' is still QA_router_cancel, the picker still reads Don't create profiles, and control-openhands api GET /api/llm/provider-connections has no QA_unused (display_name is the field).
Leave the editorF12.editor-cancelMapped behavior
Scenario F12--leave-the-editor · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoEnter “Changed: {{ instance_text }}” in “Meta profile prompt template”.
Expect
while the create or edit editor is open it replaces the list, Add Model Router and the switch; Cancel returns to the list without saving anything.
Exact CLI recipe & bug checks
In that editor control-openhands browser count on testid=meta-profile-list, testid=add-meta-profile and testid=meta-profile-run-at-conversation-start is 0 each. Pick classifier deepseek-flash, fill the prompt 'Pick: {{ instance_text }}', click testid=meta-profile-cancel: count 'testid=meta-profile-list' is 1, count 'testid=meta-profile-row-QA_router_cancel' is 0 and api GET /api/meta-profiles has no QA_router_cancel. In edit mode (row menu → Edit on QA_router), control-openhands browser fill 'testid=meta-profile-prompt-template' 'Changed: {{ instance_text }}' then Cancel leaves control-openhands api GET /api/meta-profiles/QA_router with its saved prompt_template.
Run on first messageF12.run-first-message-toggleMapped behavior
Scenario F12--run-on-first-message · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoChoose “Meta profile run at conversation start” / “Run on first message”.
- DoReload the page.
Expect
the switch saves immediately (no Save button, no toast) and survives a reload.
Exact CLI recipe & bug checks
Run control-openhands browser click 'testid=meta-profile-run-at-conversation-start >> text=Run on first message', then control-openhands browser reload and the switch eval: checked flips and stays flipped; api GET /api/settings shows run_router_at_conversation_start with the same value; control-openhands browser toasts is empty. Click again to restore true.
Routing in a conversationF12.route-first-messageMapped behavior
Scenario F12--routing-in-a-conversation · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoStart a conversation with the prompt “Run echo routed in the terminal and tell me its output.”.
- WaitWait for the element identified in the CLI recipe to appear.
- DoOpen /settings/meta-llm.
Expect
with a router active and the switch on, a new conversation routes its first message (route_task_to_model action, Switched to profile <name> in the chat); with the switch off the tool is still attached but the instruction is absent.
Exact CLI recipe & bug checks
Make QA_router active (row menu → Set active, wait-text 'Meta-profile "QA_router" activated') with the switch on. Run control-openhands conversation start --prompt "Run echo routed in the terminal and tell me its output." --wait --timeout 300, then control-openhands conversation events <id> --kinds ActionEvent,ObservationEvent,MessageEvent (<id> from the start output). The first action is route_task_to_model, its observation starts Classified task as 'model: deepseek-flash' and switched to LLM profile 'deepseek-flash', then the terminal runs echo routed. On the conversation page control-openhands browser wait-text 'Switched to profile' succeeds and control-openhands browser screenshot --feature F12.route-first-message --name conversation shows the Switched to profile deepseek-flash line above the answer. control-openhands api GET "/api/conversations/<id>/events/search?limit=1" --max-bytes 200000 (the system prompt) contains ROUTE_AT_CONVERSATION_START and "route_task_to_model". Switch Run on first message off (control-openhands browser goto /settings/meta-llm first: the browser is on the conversation), start --prompt "Reply with only the word pong.", and the same GET contains "route_task_to_model" but no ROUTE_AT_CONVERSATION_START; there is no route action.
DeleteF12.deleteMapped behavior
Scenario F12--delete · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoOpen /settings/meta-llm.
- DoChoose “Meta profile menu trigger qa router flash”.
- DoChoose “Meta profile actions menu” / “Meta profile delete”.
- DoChoose the dialog / “Cancel”.
- DoChoose “Delete meta profile confirm”.
- WaitWait for the element identified in the CLI recipe to appear.
- DoReload the page.
Expect
Delete asks Are you sure you want to delete "<name>"? This cannot be undone.; Cancel keeps the row, Delete removes it.
Exact CLI recipe & bug checks
Run control-openhands browser goto /settings/meta-llm, control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router_flash', control-openhands browser click 'testid=meta-profile-actions-menu >> testid=meta-profile-delete' and control-openhands browser snapshot 'role=dialog' (Delete meta-profile, Are you sure you want to delete "QA_router_flash"? This cannot be undone., Cancel, Delete). control-openhands browser click 'role=dialog >> role=button[name="Cancel"]' keeps the row (count 1). Repeat the menu click, then control-openhands browser click 'testid=delete-meta-profile-confirm', control-openhands browser wait-text 'Meta-profile "QA_router_flash" deleted', control-openhands browser reload; control-openhands browser count 'testid=meta-profile-row-QA_router_flash' is 0.
Delete the active routerF12.delete-activeMapped behavior
Scenario F12--delete-the-active-router · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoStart a conversation with the prompt “Reply with only the word pong.”.
Expect
deleting the active router leaves no router active; the switch shows off and disabled, and new conversations get neither the tool nor the first-message instruction.
Exact CLI recipe & bug checks
With QA_router active and the switch on, delete it the same way (wait-text 'Meta-profile "QA_router" deleted'). After a reload testid=meta-profile-empty is back, the switch eval is {checked:false, disabled:true} and api GET /api/meta-profiles has active_meta_profile null, while api GET /api/settings still has "run_router_at_conversation_start": true. A new conversation start --prompt "Reply with only the word pong." --wait has neither "route_task_to_model" nor ROUTE_AT_CONVERSATION_START in its events/search output.
Light themeF12.light-themeMapped behavior
Scenario F12--light-theme · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoStart a conversation as specified in the recipe.
- DoOpen /settings/meta-llm.
- DoOpen /settings/app.
- DoChoose “Color theme”.
- DoChoose “Light+”.
- DoOpen /settings/meta-llm.
Expect
in a light colour theme the list heading and router names are readable.
Exact CLI recipe & bug checks
No router is left after F12.delete-active, whose last conversation start left the browser on a conversation: run control-openhands browser goto /settings/meta-llm and create one through Custom as in F12.phone (name QA_router_light, classifier deepseek-flash, prompt 'Pick one model for: {{ instance_text }}', wait-text 'Meta-profile "QA_router_light" saved'), then control-openhands browser goto /settings/app, control-openhands browser click 'testid=color-theme-input', control-openhands browser click 'role=option[name="Light+"]', control-openhands browser goto /settings/meta-llm and control-openhands browser eval "(()=>{const h=[...document.querySelectorAll('h2')].find(e=>e.textContent.includes('Available meta-profiles'));const n=document.querySelector('[data-testid^=meta-profile-row-] span');return {heading:getComputedStyle(h).color,rowName:getComputedStyle(n).color,bg:getComputedStyle(document.body).backgroundColor}})()". It returns {heading: "rgb(31, 31, 31)", rowName: "rgb(31, 31, 31)", bg: "rgb(255, 255, 255)"}: dark text, the same ink as LLM profile names on /settings/llm. control-openhands browser screenshot --feature F12.light-theme --name list shows the heading Available meta-profiles and the name QA_router_light next to its summary and Active badge. Restore with role=option[name="OpenHands-Neutral"] the same way, then delete QA_router_light through its row menu (wait-text 'Meta-profile "QA_router_light" deleted') so F12.phone starts on a router-less page.
Phone (`F12.phone`, also re-proves `F12.create-first` on a router-less page)F12.phone · F12.create-firstKnown issue
Scenario F12--phone-f12-phone-also-re-proves-f12-create-first-on-a-router-less-page · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupUse the phone viewport.
- DoOpen /settings/meta-llm.
- DoChoose “Add meta profile”.
- DoChoose “Meta profile menu trigger qa router phone”.
- SetupUse the desktop viewport.
Expect
the list, template modal, editor and row menu fit a 390 px viewport without horizontal overflow.
saving the first router lists it, makes it Active and turns Run on first message on.
Linked known issues
Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).
Exact CLI recipe & bug checks
Run control-openhands browser viewport phone, control-openhands browser goto /settings/meta-llm, control-openhands browser click 'testid=add-meta-profile' and control-openhands browser bbox 'testid=meta-profile-template-modal' (insideViewport true, pageHorizontalOverflow false). Choose Custom, fill QA_router_phone, classifier deepseek-flash and prompt 'Pick one model for: {{ instance_text }}'; control-openhands browser bbox 'testid=meta-profile-editor' has no page overflow (taller than the screen, scrolls). Save, reload, then control-openhands browser bbox 'testid=meta-profile-row-QA_router_phone' and, after control-openhands browser click 'testid=meta-profile-menu-trigger-QA_router_phone', control-openhands browser bbox 'testid=meta-profile-actions-menu' are inside the viewport without page overflow. control-openhands browser screenshot --feature F12.phone --name list shows QA_route… truncated, the summary, the badge and the switch on. Return with control-openhands browser viewport desktop.
Restart persistenceF12.listKnown issue
Scenario F12--restart-persistence · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoRestart the run-owned stack as specified.
- DoOpen /settings/meta-llm.
Expect
each row shows the name, <classifier> · Direct prompt, an Active badge on the active router and a ... menu; routers and the active choice survive a stack restart.
Linked known issues
Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal ~ directory when Agent Server was configured with leading-tilde OH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal ~/... or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first, F12.list).
Exact CLI recipe & bug checks
Run control-openhands restart, control-openhands doctor, control-openhands browser goto /settings/meta-llm and control-openhands browser count 'testid=meta-profile-row-QA_router_phone >> testid=meta-profile-active-badge'; it is 1.
CleanupF12 · setup / cleanupSupport step
Scenario F12--cleanup · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- SetupArrange the prerequisite backend state through the API.
- SetupArrange the prerequisite backend state through the API.
Expect
Complete this support step before continuing to the next scenario.
Exact CLI recipe & bug checks
Switch Run on first message off through its label first (it disables once no router is left), then delete each QA_* router through the row menu. Remove the arranged LLM profile and connection with control-openhands api DELETE /api/profiles/deepseek-chat --write and control-openhands api DELETE /api/llm/provider-connections/<connection id> --write (<connection id> from control-openhands api GET /api/llm/provider-connections), or through Settings → LLM (F10).
Router activation is honest on ACPF12.activateKnown failure
Scenario F12--router-activation-is-honest-on-acp · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoACP cannot attach the router; activation must report that refusal rather than plain success. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.
Expect
Set active toasts Meta-profile "<name>" activated and moves the badge.
Known bug observations
Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.
Exact CLI recipe & bug checks
ACP cannot attach the router; activation must report that refusal rather than plain success. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5879): actual global ACP settings produced an activated toast while both stored pointers stayed null. An active ACP profile pointer alone does not create this condition. The block restores the original owned settings bytes; its assertion rejects the misleading success toast.
sh known-bug F12.activate acp-settings
QA_ACP_SETTINGS="$OH_VERIFY_RUN/private/qa-known-acp-settings.json"
cp "$OH_VERIFY_RUN/private/settings.json" "$QA_ACP_SETTINGS"
trap 'cp "$QA_ACP_SETTINGS" "$OH_VERIFY_RUN/private/settings.json"; rm -f "$QA_ACP_SETTINGS"; control-openhands api DELETE /api/meta-profiles/QA_KNOWN_ACP_ROUTER --write >/dev/null; control-openhands browser reload >/dev/null' EXIT
control-openhands api POST /api/meta-profiles/QA_KNOWN_ACP_ROUTER --data '{"classifier_model":"QA_KNOWN_CLASSIFIER","prompt_template":"Pick {{ instance_text }}"}' --write
control-openhands api PATCH /api/settings --data '{"agent_settings_diff":{"agent_kind":"acp"}}' --write --pick agent_settings.agent_kind
control-openhands assert --field value --equals acp -- api GET /api/settings --pick agent_settings.agent_kind
control-openhands browser viewport desktop
control-openhands browser goto /settings/meta-llm
control-openhands browser wait 'testid=meta-profile-row-QA_KNOWN_ACP_ROUTER'
control-openhands browser click 'testid=meta-profile-menu-trigger-QA_KNOWN_ACP_ROUTER'
control-openhands browser click 'testid=meta-profile-set-active' --observe 'role=status' --observe-ms 1000
control-openhands assert --field value --equals false --bug-shows true -- browser eval 'document.body.innerText.includes("Meta-profile \"QA_KNOWN_ACP_ROUTER\" activated")' # bug
Unreadable settings refuse router activationF12.activateKnown failure
Scenario F12--unreadable-settings-refuse-router-activation · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do — reading outline
- DoExpected: a handled409 without a filesystem path and no store overwrite. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in
EXITbefore the runner's health check.
Expect
Set active toasts Meta-profile "<name>" activated and moves the badge.
Known bug observations
Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in EXIT before the runner's health check.
Exact CLI recipe & bug checks
Expected: a handled409 without a filesystem path and no store overwrite. Known failure (reproduced twice 2026-10-16, OpenHands/software-agent-sdk#5922): the real Set active path showed the generic500 and the settings path. The corrupt bytes survived. Back up only this run's settings, and restore them in EXIT before the runner's health check.
sh known-bug F12.activate corrupt-settings
QA_ROUTER_SETTINGS="$OH_VERIFY_RUN/private/qa-known-router-settings.json"
cp "$OH_VERIFY_RUN/private/settings.json" "$QA_ROUTER_SETTINGS"
trap 'cp "$QA_ROUTER_SETTINGS" "$OH_VERIFY_RUN/private/settings.json"; rm -f "$QA_ROUTER_SETTINGS"; control-openhands api DELETE /api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER --write >/dev/null; control-openhands browser reload >/dev/null' EXIT
control-openhands api POST /api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER --data '{"classifier_model":"QA_KNOWN_CLASSIFIER","prompt_template":"Pick {{ instance_text }}"}' --write
control-openhands browser viewport desktop
control-openhands browser goto /settings/meta-llm
control-openhands browser wait 'testid=meta-profile-row-QA_KNOWN_CORRUPT_ROUTER'
printf '{"agent_settings": ' > "$OH_VERIFY_RUN/private/settings.json"
control-openhands browser click 'testid=meta-profile-menu-trigger-QA_KNOWN_CORRUPT_ROUTER'
control-openhands browser network --clear
control-openhands browser click 'testid=meta-profile-set-active' --observe 'role=status' --observe-ms 1000
control-openhands assert --field recent.0.status --equals 409 --bug 'recent.0.status eq 500 && recent.0.path eq "/api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER/activate"' -- browser network --filter '/api/meta-profiles/QA_KNOWN_CORRUPT_ROUTER/activate' # bug
Load errorF12.load-errorBehavior contract
Scenario F12--contract-f12-load-error · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
when only the meta-profile request fails, a red generic error shows under the header.
UnsupportedF12.unsupportedBehavior contract
Scenario F12--contract-f12-unsupported · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
an agent server without /api/meta-profiles (404) shows This backend doesn't support model routing yet. Update the agent server to manage meta-profiles.
CloudF12.cloudBehavior contract
Scenario F12--contract-f12-cloud · Permalink · Source
Given
Use F12’s prerequisites and the common setup. Follow the family’s order when a script uses an earlier fixture or says “as above”.
Do
This behavior has no separately named recipe in this snapshot. Use the family’s entry points and the contract below; the page does not invent a replay.
Expect
a Cloud backend without an organization shows Meta-profiles are only available for local backends.; an organization-bound Cloud backend shows the full page.
Family notes, prerequisites for known issues & limits Known issue
- Settings nav test ids embed the route: quote
'testid=sidebar-settings-/settings/meta-llm'as written. - The empty-state hint about LLM profiles only exists before any profile is saved: drive
F12.empty-statesfirst on a fresh run. - The first router you create is activated and turns Run on first message on by itself; later routers do neither. Any recipe that creates a router on an empty list changes both.
Run on first messageis ansr-onlycheckbox (keyboard reachable): click the label text insidetestid=meta-profile-run-at-conversation-startand read.checked/.disabledwithbrowser eval. The stored preference (misc_settings.app_preferences.run_router_at_conversation_start) is independent of what the disabled switch shows; with no active router it may still betrue. Turn it off before deleting the last router, or a later "first router" will not re-enable it because it is already on.- Router templates name 11 models (
gpt-5.4…claude-sonnet-4-5) that will not exist in a sandbox. With a provider connection present, a template preselects it, and saving creates all 11 as LLM profiles linked to that connection unless you pick Don't create profiles first (F12.create-moredoes). If a run created them, remove them withfor p in gpt-5.4 gpt-5.5 minimax-m3 claude-opus-4-6 claude-opus-4-7 claude-opus-4-8 claude-fable-5 kimi-k2.5 kimi-k2.6 deepseek-v3.2-reasoner claude-sonnet-4-5; do control-openhands api DELETE /api/profiles/$p --write; done(they live in the run's private state and only add rows to Settings → LLM). - Profile matching is case-insensitive and exact by name: table rows are
- <profile name>: description; existingdeepseek-flash/deepseek-proare not re-created, and the endpoint is derived as<provider lowercased>/<name>. - The classifier and the model table refer to LLM profile names, not model ids. A router whose classifier profile does not exist (for example a template's
minimax-m3without a connection) saves fine and only fails when a conversation tries to route. - The agent decides whether to call
route_task_to_model: a prompt that conflicts with routing (Reply with only the word pong.) made deepseek-flash skip the call even with the instruction present. Use a task-like prompt for the positive check and assert the system prompt viaevents/searchfor the instruction itself. conversation eventsdoes not print the system prompt; read it withcontrol-openhands api GET "/api/conversations/<id>/events/search?limit=1" --max-bytes 200000and grep the output.service stop agent-serverdoes not show this page's error state: the whole app switches to the Manage backends card (F02). Runcontrol-openhands restartafterwards.- The selection in the classifier and connection pickers is a combobox: open it with a click on the input, then click
role=option[name="..."];browser fillon the classifier sets a free-text value (allowed) instead of picking a profile. - At 390 px the row truncates the name (
QA_route…) and summary; read names withbrowser text, not the screenshot. - The Model table placeholder displays separate lines (
F12.template-custom). - Known issue OpenHands/software-agent-sdk#5878: Router files can be written under a literal
~directory when Agent Server was configured with leading-tildeOH_PERSISTENCE_DIR. Blocked on the normal absolute-path launcher fixture: needs a disposable server with private HOME/cwd and literal~/...or relative persistence configuration, then a router save and filesystem readback. Never point it at user state. (F12.create-first,F12.list).
Before you start
Use an OpenHands checkout at the source revision, with Node 24+, installed npm dependencies, uv / uvx, and a supported Chromium browser. The verification skill is the complete setup contract.
export PATH="$PWD/.agents/skills/verify-openhands/scripts:$PATH"
export OH_VERIFY_RUN=$(control-openhands launch --new --print-run)
control-openhands doctor
control-openhands onboard --skip
# Configure a model only for a scenario that needs one:
control-openhands llm preset deepseek
# When the run is complete:
control-openhands stop
F01 walks onboarding instead of skipping it. No-model checks run before model configuration. Keep OH_VERIFY_RUN set so commands address your own run. Desktop is 1440 × 1000; phone is 390 × 844. Model-backed checks need the configured provider key and budget.
Recipes run in source order within each family and can share fixtures. The page preserves that order; “as above” is a dependency on an earlier case. Setup commands and API writes arrange state. They do not prove a UI path. Follow the family cleanup steps.
A known failure preserves the intended expectation beside the observed defect. Executable known-bug blocks use xfail for a reproduced symptom and xpass when it stops reproducing. Cloud accounts, enterprise capabilities and other unavailable prerequisites remain explicit. “Not run” is different from “blocked”.
The maintained map describes application behavior at its pinned revision. Once a fix is verified in the application version under test, remove the obsolete failure note and keep the normal expectation. Dated evidence belongs in the backfill ledger.
Read the full map index and coverage boundaries, the CLI guide, or the earlier illustrated edition. The verification approach adapts Lauren Tan’s (@poteto) pstack verification-skill generators; attribution and adaptation.