EN / field notes OpenHands feature map

OpenHands / F10

LLM profiles

An LLM profile is a named model configuration: provider and model (or a custom model and base URL), an API key or a ChatGPT subscription, an optional provider-connection link, and every advanced LLM option. Under Settings → LLM a user lists profiles (grouped by provider connection once any are linked), adds and edits them in an embedded form whose Save first runs a one-token validation call, renames, duplicates, deletes and sets one as Default, and bulk-adds models from a provider. The Default profile is the model new conversations run on. Provider connections themselves (add, edit, delete) are F11.

34 mapped behaviors · 33 recipes and supporting checks · source snapshot 9 October 2026
From upstream main at 8793c111. Read the maintained source.

How to get to it

  • Sidebar settings gear (backend-selector-settings-link, opens /settings/agents), then LLM in the settings navigation (sidebar-settings-/settings/llm).
  • Direct URL /settings/llm.
  • Command menu (Control+k or Meta+k, or command-menu-trigger in the sidebar): search "LLM", choose LLM profiles ("Manage models, providers, and API keys.").
  • On the list: Add LLM Profile (add-llm-profile), Add from provider connections (add-models-from-provider), each row's ... (profile-menu-trigger), and a provider connection row's ... → Bulk add (provider-connection-add-models).
  • The create/edit editor has no URL of its own: it replaces the list in place at /settings/llm; Back (back-to-profiles) and Cancel (cancel-profile-btn) return to the list.

Before you start

Start with the common launch and health checks, then follow this family’s preconditions in order. Recipes share the fixtures and state named below.

Preconditions:

  • Baseline state (launched, doctored, onboard --skip done). Do not run llm preset deepseek first: a fresh run has no profiles, which the empty state and "first profile becomes Default" need.
  • A DeepSeek key for real saves, either in the environment as DEEPSEEK_API_KEY or in a file. Pass it only with browser fill ... --value-env DEEPSEEK_API_KEY or --value-file <key file>; never on the command line.
  • No profile or connection named QA_*, deepseek-chat or deepseek-v3 exists (control-openhands api GET /api/profiles, control-openhands api GET /api/llm/provider-connections).
  • Select a row by exact name with the CSS form '[data-testid=profile-row]:has([title="<name>"])'; has-text=QA_pro also matches QA_pro-copy.
  • Read toasts with control-openhands browser wait-text '<text>' (or browser toasts) right after the click; they disappear after a few seconds.
  • Blocked here: completing F10.subscription sign-in (needs a ChatGPT Plus/Pro account), F10.cloud-readonly (needs a Cloud org member account), F10.cloud-provider-pagination (needs a Cloud organization backend where the user can manage profiles), F10.schema-unavailable (needs an agent server without the schema endpoints, for example an older launch --sdk-ref), and the F10.list-states error (needs /api/profiles to fail while the rest of the agent server answers).

Behavior inventory

34 stable behavior IDs and their expected behavior
  • F10.list the "Available Profiles" list shows each row's name, model, a Default badge on the active profile and a ... menu, sorted by name; reachable from the sidebar, the direct URL and the command menu. Read recipe ↓
  • F10.list-empty with no profiles the list reads No profiles saved yet. Add a profile to save your LLM configurations. and both Add buttons stay available. Read recipe ↓
  • F10.list-states a spinner shows while profiles load; a load failure shows Failed to load profiles. Read recipe ↓
  • F10.list-grouped once any profile links to a provider connection, rows group under the connection's name, with unlinked rows last under Not linked; with no linked rows the list is flat again. Read recipe ↓
  • F10.actions-menu the row menu offers Edit, Rename, Duplicate, Set as default (disabled on the Default row) and Delete; it focuses Edit on open (mouse or Enter), arrows cycle over the enabled items (skipping a disabled Set as default), and Escape, Tab or an outside click close it. Read recipe ↓
  • F10.set-default Set as default toasts Switched to profile "<name>", moves the badge, persists and becomes the agent's active LLM. Read recipe ↓
  • F10.duplicate Duplicate saves <name>-copy, then <name>-copy-1, with the same model and stored key; the copy is not Default. Read recipe ↓
  • F10.rename the Rename modal is prefilled and focused; invalid names disable Rename; an unchanged name or Cancel just closes; Enter submits; success toasts and persists, and the Default badge follows a renamed default. Read recipe ↓
  • F10.rename-conflict renaming onto an existing name is refused with the server's message (Profile '<name>' already exists) and the modal stays open. Read recipe ↓
  • F10.delete Delete asks Are you sure you want to delete the profile "<name>"? This action cannot be undone. with Cancel focused; Cancel keeps the row, Delete removes it with a toast. Read recipe ↓
  • F10.delete-default deleting the Default profile promotes another remaining profile to Default. Read recipe ↓
  • F10.create Add LLM Profile → provider, model and API key → Save shows Validating... then Saving..., toasts Profile "<name>" created and returns to the list; the first profile on an empty list becomes Default. Read recipe ↓
  • F10.create-prefill a new editor prefills the backend default model with a blank key, base URL and connection; the other LLM options it shows come from the agent's current LLM settings, i.e. the last applied Default profile (the editor opens on the tab they need), and Save stores what the form shows. Read recipe ↓
  • F10.api-key-help under the API key field (Basic and Advanced), Don't know your API key? Click here for instructions opens the OpenHands docs on getting an API key in a new tab. Read recipe ↓
  • F10.name-autofill the Profile Name is auto-filled from the chosen model and follows later model changes until the user types a name; clearing the field resumes it. Read recipe ↓
  • F10.name-validation a name that breaks the rule (letter or digit first; letters, digits, ., _, -; max 64) disables Save and turns the rule text red; a name that already exists also disables Save. Read recipe ↓
  • F10.create-validation a Save whose validation call fails toasts the provider error and keeps the editor open with nothing saved. Read recipe ↓
  • F10.custom-model the Advanced tab takes a free-text model and Base URL; an empty model is refused with Model is required. Read recipe ↓
  • F10.model-picker the Basic provider and model comboboxes group options as Verified Models and Other Models; changing provider clears the model; the OpenHands provider shows account and key help. Read recipe ↓
  • F10.provider-list-complete the Basic provider combobox offers every provider the backend reports or has verified models for (over a hundred, xai included); picking a late-sorting one fills the field and loads its models. Read recipe ↓
  • F10.cloud-provider-pagination on a Cloud organization backend the provider list is fetched from providers/search page by page, so providers past the first page (xai, openrouter) are offered too (blocked here). Read recipe ↓
  • F10.edit Edit loads the profile (Editing profile "<name>" - save to apply changes) with the stored key masked; Save stays disabled until something changes; a changed name renames before saving; the stored key is kept. Read recipe ↓
  • F10.basic-save-keeps-base-url re-saving a profile from the Basic view without changing its model keeps the Base URL that view hides. Read recipe ↓
  • F10.edit-default-reapply saving the Default profile re-applies it to the agent's LLM settings. Read recipe ↓
  • F10.editor-discard Back and Cancel return to the list without saving and without an unsaved-changes prompt. Read recipe ↓
  • F10.schema-validation bad values in schema fields (Temperature over 2, Top P over 1, invalid JSON) are refused with a toast naming the field. Read recipe ↓
  • F10.add-models "Add from provider connections" (or a connection row's Bulk add) creates one linked or keyless profile per checked model; models that already have a profile of that name are hidden. Read recipe ↓
  • F10.subscription Authentication → ChatGPT subscription swaps the model fields for a subscription model list and a device-login card (code, sign-in link, poll, cancel); switching back restores the API-key model. Read recipe ↓
  • F10.schema-unavailable a backend without the settings-schema endpoints shows SDK settings schema unavailable. with an upgrade or session-key hint.
  • F10.cloud-readonly Cloud org members without edit rights see the list read-only (no Add buttons, no row menus, no Provider connections section).
  • F10.phone list, row menu and editor fit a 390 px viewport without horizontal overflow. Read recipe ↓
  • F10.default-used a conversation started after creating a profile in the UI runs on the Default profile's model. Read recipe ↓

Readable recipes

Read each script from top to bottom. Code is copied from the map; prose gives the action, expected observation, and conditions. <id>, <run> and similar placeholders stand for values from your own run. Short forms such as browser count continue the same control-openhands invocation; they are kept as documented.

Expected observations describe the recipe’s contract. Captures below selected recipes show representative real states from this snapshot; they do not mark every mapped behavior as passed. Follow cleanup before moving to another family.

Empty list #

  1. Note
    On a fresh run, navigate from the sidebar:
  2. Check
    control-openhands browser click 'testid=backend-selector-settings-link' --expect-url '/settings'
  3. Check
    control-openhands browser click 'testid=sidebar-settings-/settings/llm' --expect-url '/settings/llm'
  4. Check
    control-openhands browser text 'testid=profiles-empty'
  5. Expect
    The text is No profiles saved yet. Add a profile to save your LLM configurations. and
  6. Check
    control-openhands browser testids main

    lists add-llm-profile, add-models-from-provider and profiles-empty.

LLM settings shows no saved profiles and no provider connections.
Profiles save model configurations; provider connections can share one key across models. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

LLM settings shows no saved profiles and no provider connections.

Representative documentation capture, not a complete run of this recipe or family. Captured on an isolated local backend at desktop viewport using the current main checkout.

How this screenshot was taken

canvas: 1.26.0 · agent server: 1.53.0 · sdk: 1.53.0 · automation: 1.19.0

OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser goto /settings/llm
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=add-llm-profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F10.list-empty --name llm-empty

Open the editor #

  1. Do
    control-openhands browser click 'testid=add-llm-profile'
  2. Check
    control-openhands browser text 'testid=profile-editor-title'

    is Add LLM Profile,

  3. Check
    control-openhands browser text 'testid=profile-editor-description'

    is Configure your LLM settings below, then click Save to create this profile., and the form opens on the Basic tab with provider OpenAI and model gpt-5.6-sol (browser value 'testid=llm-provider-input', browser value 'testid=llm-model-input').

Add LLM Profile editor showing a profile name, OpenAI provider, gpt-5.6-sol model and an empty API key field.
Add a profile opens a readable form with Basic and All configuration views. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

Add LLM Profile editor showing a profile name, OpenAI provider, gpt-5.6-sol model and an empty API key field.

Representative documentation capture, not a complete run of this recipe or family. Captured on an isolated local backend at desktop viewport using the current main checkout.

How this screenshot was taken

canvas: 1.26.0 · agent server: 1.53.0 · sdk: 1.53.0 · automation: 1.19.0

OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser goto /settings/llm
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=add-llm-profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser click testid=add-llm-profile
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=profile-editor-title
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F10.create --name llm-editor

API key help link #

  1. Note
    In that editor run
  2. Do
    control-openhands browser click 'testid=sdk-section-basic-toggle'

    (a new editor opens on All once the agent's LLM settings carry a non-default option; see F10.create-prefill), then

  3. Check
    control-openhands browser text 'testid=llm-api-key-help-anchor'

    is Don't know your API key? Click here for instructions; its link has target _blank (browser eval "document.querySelector('[data-testid=llm-api-key-help-anchor] a, a[data-testid=llm-api-key-help-anchor]').target").

  4. Do
    control-openhands browser click 'testid=llm-api-key-help-anchor >> role=link'
  5. Do
    control-openhands browser tabs

    (rerun once if it still lists one page): page 1 is https://docs.openhands.dev/.../local-setup#getting-an-api-key.

  6. Note
    Return with
  7. Do
    control-openhands browser close-tab 1
  8. Do
    control-openhands browser tab 0
  9. Expect
    The Advanced tab shows the same link as testid=llm-api-key-help-anchor-advanced.

Provider and model lists #

  1. Do
    control-openhands browser click 'testid=llm-provider-input'
  2. Check
    control-openhands browser snapshot 'role=listbox'
  3. Note
    : groups Verified Models (OpenHands, Anthropic, OpenAI, Mistral AI, Gemini, deepseek, Moonshot, minimax, glm, nvidia, qwen) and Other Models.
  4. Note
    Pick DeepSeek with
  5. Do
    control-openhands browser fill 'testid=llm-provider-input' deepseek
  6. Do
    control-openhands browser click 'testid=provider-item-deepseek'
  7. Note
    browser value 'testid=llm-model-input' is now empty.
  8. Do
    control-openhands browser click 'testid=llm-model-input'
  9. Note
    and browser snapshot 'role=listbox' list deepseek-chat, deepseek-v4-pro, … as Verified and deepseek-coder, deepseek-flash, … under Other.
  10. Note
    For the OpenHands provider (browser fill 'testid=llm-provider-input' openhands, browser click 'testid=provider-item-openhands'),
  11. Check
    control-openhands browser text 'testid=openhands-account-help'

    is Need an OpenHands Account? Click here; after

  12. Do
    control-openhands browser click 'role=option[name="claude-sonnet-5"][exact]'
  13. Note
    in the model list,
  14. Check
    control-openhands browser text 'testid=openhands-api-key-help'
  15. Note
    starts You can find your OpenHands Provider LLM Key in the OpenHands LLM Key section of OpenHands Cloud.
  16. Note
    Screenshot: browser screenshot --feature F10.model-picker --name openhands-provider.

Every provider is offered #

  1. Do
    control-openhands browser click 'testid=cancel-profile-btn'

    (the Provider and model lists bullet leaves its editor open; skip this when the list is showing),

  2. Do
    control-openhands browser click 'testid=add-llm-profile'
  3. Do
    control-openhands browser click 'testid=sdk-section-basic-toggle'
  4. Do
    control-openhands browser click 'testid=llm-provider-input'
  5. Check
    control-openhands browser count 'role=listbox >> role=option'

    is the size of the union of the two backend lists the page merges: the names in

  6. Check
    control-openhands api GET /api/llm/providers --pick providers

    (under value; control-openhands api GET /api/llm/providers --pick providers | jq '.value | length' is 160 today) and the provider keys of

  7. Check
    control-openhands api GET /api/llm/models/verified --max-bytes 200000

    (its body is {"models": {<provider>: [<model>, …]}}; control-openhands api GET /api/llm/models/verified --max-bytes 200000 | jq '.body.models | keys | length' is 12 today, four of which, glm, nvidia, openhands and qwen, are missing from the first list).

  8. Expect
    The union is jq -s '([.[0].value[]] + (.[1].body.models | keys)) | unique | length' <(control-openhands api GET /api/llm/providers --pick providers) <(control-openhands api GET /api/llm/models/verified --max-bytes 200000) (bash process substitution): 164 today, and the listbox count is the same 164, with every backend name present under its display label (openhands as OpenHands, mistral as Mistral AI, azure_ai as Azure AI Studio).
  9. Check
    control-openhands browser count 'role=option[name="xai"][exact]'

    is 1 (it sorts last but three, before xiaomi_mimo, xinference and zai).

  10. Note
    Pick it with
  11. Do
    control-openhands browser click 'role=option[name="xai"][exact]'
  12. Check
    control-openhands browser value 'testid=llm-provider-input'

    is xai,

  13. Check
    control-openhands browser value 'testid=llm-model-input'

    is empty, and after

  14. Do
    control-openhands browser click 'testid=llm-model-input'
  15. Note
    the listbox offers grok-4.20, grok-4.20-0309, … under Other Models (57 today; control-openhands browser screenshot --feature F10.provider-list-complete --name xai-picked).
  16. Note
    Close with
  17. Do
    control-openhands browser press Escape

    (the open listbox would swallow the next click) and

  18. Do
    control-openhands browser click 'testid=cancel-profile-btn'
  19. Note
    the list is back.

Cloud provider pagination · Blocked prerequisite #

  1. Note
    Blocked: needs a Cloud organization backend where the user can manage profiles (local backends serve the whole list in one GET /api/llm/providers).
  2. Note
    There, the Basic provider list comes from providers/search one page at a time (control-openhands browser network --filter providers/search lists one request per page, the later ones with a page_id), and xai and openrouter, which sort past the first page, are offered and pick like any other provider.
  3. Note
    Record
  4. Do
    control-openhands evidence add --feature F10.cloud-provider-pagination --result blocked ...
  5. Note
    naming the Cloud account.

Create the first profile #

  1. Note
    In a fresh editor pick deepseek as above, then
  2. Do
    control-openhands browser click 'testid=llm-model-input'
  3. Do
    control-openhands browser click 'testid=model-item-deepseek-flash'
  4. Check
    control-openhands browser value 'testid=profile-name-input'
  5. Note
    now follows the model: deepseek-flash (right after the provider pick, while the model is still empty, it still reads gpt-5.6-sol).
  6. Do
    control-openhands browser fill 'testid=profile-name-input' QA_flash
  7. Check
    control-openhands browser fill 'testid=llm-api-key-input' --value-file <key file>

    (or --value-env DEEPSEEK_API_KEY),

  8. Do
    control-openhands browser click 'testid=save-profile-btn' --observe 'testid=save-profile-btn' --observe-ms 4000
  9. Wait
    control-openhands browser wait-text 'Profile "QA_flash" created' --timeout 60000
  10. Expect
    The observed button states are Save, Validating..., Saving..., then the editor closes.
  11. Note
    After
  12. Do
    control-openhands browser reload
  13. Check
    control-openhands browser text '[data-testid=profile-row]:has([title="QA_flash"])'

    reads QA_flash, deepseek/deepseek-flash, Default, and

  14. Check
    control-openhands api GET /api/profiles

    has active_profile QA_flash.

A typed name sticks; a cleared one follows again #

  1. Note
    In a fresh editor (control-openhands browser click 'testid=add-llm-profile') run
  2. Do
    control-openhands browser fill 'testid=profile-name-input' QA_typed
  3. Note
    then pick deepseek as above,
  4. Do
    control-openhands browser click 'testid=llm-model-input'
  5. Do
    control-openhands browser click 'role=option[name="deepseek-v4-pro"][exact]'
  6. Note
    browser value 'testid=profile-name-input' stays QA_typed.
  7. Do
    control-openhands browser fill 'testid=profile-name-input' ''
  8. Note
    then browser click 'testid=llm-model-input' and browser click 'testid=model-item-deepseek-flash': the name is deepseek-flash; pick role=option[name="deepseek-v4-pro"][exact] again and it is deepseek-v4-pro.

Name rules #

  1. Note
    In the editor run
  2. Do
    control-openhands browser fill 'testid=profile-name-input' 'QA bad'
  3. Check
    control-openhands browser enabled 'testid=save-profile-btn'

    is false and

  4. Check
    control-openhands browser attr 'testid=profile-name-input' aria-invalid

    is true (the same for -QA and a 65-character name), and the rule text under the field turns red.

  5. Expect
    An existing name (QA_flash) also gives enabled false, but aria-invalid stays false and nothing says why (see Gotchas). QA_flash2 gives true.

Validation failure #

  1. Note
    In a fresh editor choose deepseek / model-item-deepseek-flash, fill profile-name-input with QA_nokey, leave the API key empty and run
  2. Do
    control-openhands browser click 'testid=save-profile-btn' --observe 'testid=save-profile-btn' --observe-ms 3000

    (states Save, Validating..., Save).

  3. Wait
    control-openhands browser wait-text 'Authentication Fails'
  4. Note
    finds the toast litellm.AuthenticationError: AuthenticationError: DeepseekException - Authentication Fails (...); browser text 'testid=profile-editor-title' is still Add LLM Profile and
  5. Check
    control-openhands api GET /api/profiles

    has no QA_nokey.

Custom model #

  1. Note
    In that editor run
  2. Do
    control-openhands browser click 'testid=sdk-section-advanced-toggle'
  3. Note
    browser value 'testid=llm-custom-model-input' carries the Basic choice (deepseek/deepseek-flash; placeholder openai/gpt-5.6-sol), and browser attr 'testid=base-url-input' placeholder is https://api.openai.com.
  4. Do
    control-openhands browser fill 'testid=llm-custom-model-input' ''
  5. Do
    control-openhands browser fill 'testid=profile-name-input' QA_adv
  6. Do
    control-openhands browser click 'testid=save-profile-btn'
  7. Wait
    control-openhands browser wait-text 'Model is required'
  8. Do
    control-openhands browser fill 'testid=llm-custom-model-input' deepseek/deepseek-flash
  9. Check
    control-openhands browser fill 'testid=base-url-input' https://api.deepseek.com
  10. Note
    fill the key as in Create, Save, and
  11. Wait
    control-openhands browser wait-text 'Profile "QA_adv" created' --timeout 60000
  12. Note
    api GET /api/profiles shows QA_adv with base_url https://api.deepseek.com.

Second profile from a verified model #

  1. Note
    Repeat Create choosing
  2. Do
    control-openhands browser click 'role=option[name="deepseek-v4-pro"][exact]'

    (verified models have no model-item-* test ID) and the name QA_pro.

  3. Expect
    The toast reads Profile "QA_pro" created; the row QA_pro deepseek/deepseek-v4-pro has no badge.

Row menu #

  1. Do
    control-openhands browser click '[data-testid=profile-row]:has([title="QA_flash"]) >> testid=profile-menu-trigger'
  2. Check
    control-openhands browser snapshot 'testid=profile-actions-menu'
  3. Note
    : menuitems Edit, Rename, Duplicate, Set as default [disabled], Delete on the Default row (all enabled on other rows).
  4. Do
    control-openhands browser eval "document.activeElement.dataset.testid"

    is profile-edit: focus moves into the menu on open.

  5. Do
    control-openhands browser press ArrowDown
  6. Note
    and that eval four times: profile-rename, profile-duplicate, profile-delete (the disabled Set as default is skipped), profile-edit (wraps);
  7. Do
    control-openhands browser press ArrowUp
  8. Note
    twice gives profile-delete, profile-duplicate.
  9. Do
    control-openhands browser press Tab
  10. Note
    closes the menu (browser count 'testid=profile-actions-menu' is 0) and moves focus on to the next row's trigger: the row-aware eval
  11. Do
    control-openhands browser eval "(()=>{const a=document.activeElement;const r=a.closest('[data-testid=profile-row]');return a.dataset.testid+'@'+(r?r.querySelector('[title]').title:'-')})()"

    is profile-menu-trigger@QA_pro.

  12. Note
    Reopen the QA_flash menu and
  13. Do
    control-openhands browser press Escape
  14. Note
    : count 0, and the row-aware eval is profile-menu-trigger@QA_flash (focus is back on its own trigger).
  15. Note
    Reopen it and
  16. Do
    control-openhands browser click 'role=heading[name="Available Profiles"]'
  17. Note
    closes it too.
  18. Note
    Keyboard open:
  19. Do
    control-openhands browser focus '[data-testid=profile-row]:has([title="QA_pro"]) >> testid=profile-menu-trigger'
  20. Do
    control-openhands browser press Enter
  21. Note
    put focus on profile-edit; on this non-default row
  22. Check
    control-openhands browser snapshot 'testid=profile-actions-menu'

    lists the same five menuitems with none [disabled], and

  23. Do
    control-openhands browser screenshot --feature F10.actions-menu --name menu-nondefault

    shows them with the focus ring on Edit.

  24. Note
    Five ArrowDowns walk profile-rename, profile-duplicate, profile-set-active, profile-delete, profile-edit.
  25. Do
    control-openhands browser press Escape
  26. Note
    closes it, and the row-aware eval is profile-menu-trigger@QA_pro.

Set as default #

  1. Do
    control-openhands browser click '[data-testid=profile-row]:has([title="QA_pro"]) >> testid=profile-menu-trigger'
  2. Do
    control-openhands browser click 'testid=profile-actions-menu >> testid=profile-set-active'
  3. Wait
    control-openhands browser wait-text 'Switched to profile "QA_pro"'
  4. Note
    After
  5. Do
    control-openhands browser reload
  6. Check
    control-openhands browser count 'testid=profile-active-badge'

    is 1 and

  7. Check
    control-openhands browser count '[data-testid=profile-row]:has([title="QA_pro"]) >> testid=profile-active-badge'

    is 1;

  8. Check
    control-openhands api GET /api/profiles

    has active_profile QA_pro and

  9. Check
    control-openhands llm show
  10. Note
    reports active.model deepseek/deepseek-v4-pro.
  11. Expect
    A profile's Base URL follows it into the agent settings: QA_adv (Custom model above) carries one: Set as active on its row makes
  12. Check
    control-openhands llm show
  13. Note
    report active.base_url https://api.deepseek.com, also under agent_settings.llm.base_url in
  14. Check
    control-openhands api GET /api/settings
  15. Note
    With a key,
  16. Wait
    control-openhands conversation start --prompt "Reply with only: adv-ok. Do not run any tools." --wait --timeout 240
  17. Note
    then shows the pill QA_adv and the reply adv-ok (driven 2026-10-08; QA_pro likewise replied pro-ok on deepseek/deepseek-v4-pro).
  18. Note
    Make QA_pro the Default again before Duplicate.

Duplicate #

  1. Note
    From the QA_pro row menu click testid=profile-actions-menu >> testid=profile-duplicate and
  2. Wait
    control-openhands browser wait-text 'Profile duplicated as "QA_pro-copy"'
  3. Note
    repeat on QA_pro for Profile duplicated as "QA_pro-copy-1", and on QA_pro-copy for Profile duplicated as "QA_pro-copy-copy".
  4. Expect
    After a reload
  5. Check
    control-openhands browser text 'testid=profile-row >> has-text=QA_pro-copy-1'

    reads QA_pro-copy-1 deepseek/deepseek-v4-pro, the badge stays on QA_pro, and api GET /api/profiles has api_key_set: true on every copy.

Rename #

  1. Note
    Open the QA_pro-copy-copy row menu and click testid=profile-actions-menu >> testid=profile-rename.
  2. Check
    control-openhands browser value 'testid=rename-profile-input'

    is QA_pro-copy-copy and browser eval "document.activeElement.dataset.testid" is rename-profile-input.

  3. Do
    control-openhands browser fill 'testid=rename-profile-input' '-bad'
  4. Note
    makes
  5. Check
    control-openhands browser enabled 'role=dialog[name="Rename Profile"] >> role=button[name="Rename"]'
  6. Note
    false and
  7. Check
    control-openhands browser attr 'testid=rename-profile-rule' class
  8. Note
    contains text-red-400.
  9. Note
    Filling the old name and clicking Rename closes the modal with no toast (browser count 'testid=rename-profile-modal' is 0);
  10. Do
    control-openhands browser click 'role=dialog[name="Rename Profile"] >> role=button[name="Cancel"]'
  11. Note
    also closes it.
  12. Note
    To rename, reopen, fill QA_renamed,
  13. Do
    control-openhands browser press Enter
  14. Wait
    control-openhands browser wait-text 'Profile renamed to "QA_renamed"'
  15. Note
    reload, and the count of '[data-testid=profile-row]:has([title="QA_renamed"])' is 1 while the old name's is 0.
  16. Note
    Renaming the Default row (QA_pro → QA_pro2, submit with testid=rename-profile-submit) keeps its badge (browser text '[data-testid=profile-row]:has([data-testid=profile-active-badge])' reads QA_pro2 … Default) and active_profile follows.

Rename onto an existing name #

  1. Expect
    The Rename step above already renamed QA_pro-copy-copy and QA_pro, so use the remaining fixtures: open the QA_pro-copy row menu, click testid=profile-actions-menu >> testid=profile-rename, fill testid=rename-profile-input with QA_pro2 and click testid=rename-profile-submit.
  2. Expect
    The modal stays open (count 'testid=rename-profile-modal' is 1),
  3. Check
    control-openhands browser toasts

    has the server's message Profile 'QA_pro2' already exists (no HTTP request failed prefix, no JSON), and browser screenshot --feature F10.rename-conflict --name toast shows that toast above the open modal.

  4. Note
    Close it with
  5. Do
    control-openhands browser click 'role=dialog[name="Rename Profile"] >> role=button[name="Cancel"]'

Edit #

  1. Note
    Open the QA_renamed row menu and click testid=profile-actions-menu >> testid=profile-edit. browser text 'testid=profile-editor-title' is Edit LLM Profile, browser text 'testid=profile-editor-description' is Editing profile "QA_renamed" - save to apply changes, browser attr 'testid=llm-api-key-input' placeholder is <hidden>, browser count 'testid=set-indicator' is 1 and browser enabled 'testid=save-profile-btn' is false (screenshot --feature F10.edit --name loaded).
  2. Do
    control-openhands browser click 'testid=llm-model-input'
  3. Do
    control-openhands browser click 'testid=model-item-deepseek-flash'

    (Save turns true),

  4. Do
    control-openhands browser fill 'testid=profile-name-input' QA_edited
  5. Note
    Save, and
  6. Wait
    control-openhands browser wait-text 'Profile "QA_edited" updated' --timeout 60000
  7. Expect
    After a reload browser text '[data-testid=profile-row]:has([title="QA_edited"])' reads QA_edited deepseek/deepseek-flash and QA_renamed is gone.
  8. Expect
    The save passed validation without retyping the key, which proves the stored key was kept.

Basic re-save keeps a hidden Base URL #

  1. Note
    QA_adv carries base_url https://api.deepseek.com (Custom model above).
  2. Note
    Open its row menu, click testid=profile-actions-menu >> testid=profile-edit, then
  3. Do
    control-openhands browser click 'testid=sdk-section-advanced-toggle'
  4. Check
    control-openhands browser value 'testid=base-url-input'

    is https://api.deepseek.com.

  5. Do
    control-openhands browser click 'testid=sdk-section-basic-toggle'
  6. Check
    control-openhands browser count 'testid=base-url-input'

    is 0 (the view hides it),

  7. Check
    control-openhands browser value 'testid=llm-model-input'

    is deepseek-flash and

  8. Check
    control-openhands browser enabled 'testid=save-profile-btn'

    is false (control-openhands browser screenshot --feature F10.basic-save-keeps-base-url --name basic-view).

  9. Note
    Without touching the model, run
  10. Check
    control-openhands browser fill 'testid=llm-api-key-input' --value-env DEEPSEEK_API_KEY

    (Save turns true),

  11. Do
    control-openhands browser click 'testid=save-profile-btn' --observe 'testid=save-profile-btn' --observe-ms 4000

    (Save, Validating..., Saving...) and

  12. Wait
    control-openhands browser wait-text 'Profile "QA_adv" updated' --timeout 60000
  13. Check
    control-openhands api GET /api/profiles/QA_adv --pick config.base_url

    is still https://api.deepseek.com and

  14. Check
    control-openhands api GET /api/profiles/QA_adv --pick config.model

    is deepseek/deepseek-flash; after

  15. Do
    control-openhands browser reload
  16. Note
    the same GETs agree.
  17. Note
    Without a key, arrange QA_DUMMY_KEY=qa control-openhands llm set --profile QA_base --model deepseek/deepseek-chat --api-key-env QA_DUMMY_KEY --base-url https://api.deepseek.com --no-validate --no-activate (it prints activated false and control-openhands api GET /api/profiles --pick active_profile is unchanged: the Default stays where it is and the editors below inherit it, as long as some profile is Default; on a run where QA_base is the only profile the page makes it Default at the next load, see Gotchas) and
  18. Do
    control-openhands browser reload
  19. Note
    then drive the same steps on QA_base's row with its own readbacks: its editor opens on Advanced (the Base URL is a non-default value) with
  20. Check
    control-openhands browser value 'testid=base-url-input'
  21. Note
    https://api.deepseek.com, Basic hides it (count 0),
  22. Check
    control-openhands browser value 'testid=llm-model-input'

    is deepseek-chat and

  23. Check
    control-openhands browser enabled 'testid=save-profile-btn'

    is false; fill the key with QA_DUMMY_KEY=qa-dummy-key control-openhands browser fill 'testid=llm-api-key-input' --value-env QA_DUMMY_KEY (Save turns true) and Save: the observed states are Save, Validating... ([disabled] [busy]), Save within a second,

  24. Check
    control-openhands browser toasts

    has the provider's message litellm.BadRequestError: DeepseekException - {"error":{"message":"Authentication Fails, Your api key: ****-key is invalid …, browser text 'testid=profile-editor-title' is still Edit LLM Profile,

  25. Check
    control-openhands api GET /api/profiles/QA_base --pick config.base_url

    is still https://api.deepseek.com and --pick config.model is deepseek/deepseek-chat; record blocked with DEEPSEEK_API_KEY as the prerequisite, leave with

  26. Do
    control-openhands browser click 'testid=cancel-profile-btn'
  27. Note
    and remove the arranged profile with
  28. Arrange
    control-openhands api DELETE /api/profiles/QA_base --write

    (arrange, not proof).

Leave without saving #

  1. Note
    Edit QA_adv,
  2. Do
    control-openhands browser click 'testid=sdk-section-all-toggle'
  3. Do
    control-openhands browser fill 'testid=sdk-settings-llm.top_p' 0.5

    (Save turns enabled), then

  4. Do
    control-openhands browser click 'testid=back-to-profiles'
  5. Expect
    The list returns at once (browser count 'testid=add-llm-profile' is 1) with no prompt (control-openhands browser events --kinds dialog is empty), and reopening Edit → All shows top_p empty. testid=cancel-profile-btn behaves the same.

Schema field validation and re-apply #

  1. Note
    Edit the Default profile and run
  2. Do
    control-openhands browser click 'testid=sdk-section-all-toggle'
  3. Do
    control-openhands browser fill 'testid=sdk-settings-llm.temperature' 5
  4. Note
    Save, then
  5. Wait
    control-openhands browser wait-text 'must be at most 2'

    (toast Temperature must be at most 2).

  6. Note
    Clear it, fill testid=sdk-settings-llm.top_p with 3: Top P must be at most 1.
  7. Note
    Clear it, fill testid=sdk-settings-llm.extra_headers with {bad: Invalid JSON for Extra Headers.
  8. Note
    Clear that, fill temperature 0.3, Save, and wait-text 'Profile "<name>" updated'.
  9. Check
    control-openhands api GET /api/settings
  10. Note
    then has agent_settings.llm.temperature 0.3, and reopening Edit → All shows 0.3.

New profile prefill #

  1. Note
    With that Default (temperature 0.3) run
  2. Do
    control-openhands browser click 'testid=add-llm-profile'
  3. Expect
    The editor opens on All (browser eval "[...document.querySelectorAll('[role=tab]')].map(t=>t.innerText+':'+t.getAttribute('aria-selected')).join(',')" is Basic:false,Advanced:false,All:true), browser value 'testid=llm-custom-model-input' is the backend default openai/gpt-5.6-sol, and browser value 'testid=sdk-settings-llm.temperature' is 0.3, inherited from the Default.
  4. Do
    control-openhands browser fill 'testid=llm-custom-model-input' deepseek/deepseek-flash
  5. Note
    browser fill 'testid=profile-name-input' QA_inherit, fill the key as in Create, Save and wait-text 'Profile "QA_inherit" created' --timeout 60000.
  6. Note
    Expected: the saved profile has the temperature the form showed.
  7. Note
    Actual today:
  8. Check
    control-openhands api GET /api/profiles/QA_inherit

    has config.temperature null, and after browser reload Edit QA_inherit → All shows the temperature empty (fail; the create path saves dirty fields only, so the shown inherited values are dropped).

  9. Note
    Screenshot --feature F10.create-prefill --name edit-after-create.

Link to a provider connection #

  1. Note
    Arrange a connection through the F11 form:
  2. Do
    control-openhands browser click 'testid=add-provider-connection'
  3. Do
    control-openhands browser fill 'testid=provider-connection-name-input' QA_ds_conn
  4. Do
    control-openhands browser click 'testid=provider-connection-provider-input'
  5. Do
    control-openhands browser fill 'testid=provider-connection-provider-input' deepseek
  6. Do
    control-openhands browser click 'testid=provider-item-deepseek'
  7. Check
    control-openhands browser fill 'testid=provider-connection-api-key-input' --value-file <key file>
  8. Do
    control-openhands browser click 'testid=provider-connection-submit'
  9. Wait
    control-openhands browser wait-text 'Connection "QA_ds_conn" created'
  10. Note
    Edit QA_adv, click testid=sdk-section-basic-toggle, run
  11. Do
    control-openhands browser click 'testid=llm-provider-connection-input'

    (options None, QA_ds_conn) and

  12. Do
    control-openhands browser click 'role=option[name="QA_ds_conn"]'
  13. Note
    browser count 'testid=llm-api-key-input' is 0, and on the Advanced tab browser count 'testid=base-url-input' is 0.
  14. Note
    Picking role=option[name="None"] brings the key field back (1).
  15. Note
    Re-select QA_ds_conn, then
  16. Do
    control-openhands browser click 'testid=save-profile-btn' --observe 'testid=save-profile-btn' --observe-ms 3000
  17. Note
    : states Save, Saving... (no Validating...), and wait-text 'Profile "QA_adv" updated'.
  18. Expect
    After a reload
  19. Do
    control-openhands browser eval "[...document.querySelectorAll('[data-testid=profile-group-header]')].map(e=>e.innerText).join(' | ')"

    is QA_DS_CONN | NOT LINKED (CSS upper-case) and

  20. Check
    control-openhands browser count 'div:has(> [data-testid=profile-group-header]:text-is("QA_ds_conn")) [data-testid=profile-row]:has([title="QA_adv"])'

    is 1 (screenshot --feature F10.list-grouped --name grouped).

Add models from a provider #

  1. Do
    control-openhands browser click 'testid=add-models-from-provider'
  2. Note
    browser enabled 'testid=add-models-submit' is false.
  3. Note
    Keyless:
  4. Do
    control-openhands browser select 'testid=add-models-provider' moonshot
  5. Wait
    control-openhands browser wait 'testid=add-models-select-all'
  6. Note
    browser text 'testid=add-models-keyless-note' is No connection — profiles will need a key added later, rows start unchecked (Add 0 profiles), and
  7. Do
    control-openhands browser check 'testid=add-models-select-all'
  8. Note
    makes browser text 'testid=add-models-submit' read Add 12 profiles; uncheck it again.
  9. Note
    Linked:
  10. Do
    control-openhands browser select 'testid=add-models-provider' deepseek
  11. Note
    the connection field (testid=add-models-connection-field, options No connection (keyless) and QA_ds_conn) is pre-bound to QA_ds_conn.
  12. Do
    control-openhands browser check 'testid=add-models-check-deepseek/deepseek-chat'
  13. Do
    control-openhands browser check 'testid=add-models-check-deepseek/deepseek-v3'
  14. Do
    control-openhands browser click 'testid=add-models-submit'
  15. Wait
    control-openhands browser wait-text 'Added 2'

    (toast Added 2 profiles).

  16. Expect
    The modal closes; after a reload deepseek-chat and deepseek-v3 sit under the QA_DS_CONN header and api GET /api/profiles shows both with a provider_connection_id.
  17. Note
    Preselect entry:
  18. Do
    control-openhands browser click 'testid=provider-connection-row >> has-text=QA_ds_conn >> testid=provider-connection-menu-trigger'

    (menu: Bulk add, Edit, Delete),

  19. Do
    control-openhands browser click 'testid=provider-connection-actions-menu >> testid=provider-connection-add-models'
  20. Note
    provider deepseek and the connection are preset, every remaining row is checked (Add 10 profiles), and browser count 'testid=add-models-row-deepseek/deepseek-chat' is 0 (already added).
  21. Note
    Close with
  22. Do
    control-openhands browser click 'role=dialog[name="Add models as profiles"] >> role=button[name="Cancel"]'

Broken link #

  1. Expect
    The local server refuses to delete a referenced connection (row menu → testid=provider-connection-delete → testid=delete-provider-connection-confirm toasts Provider connection cannot be deleted while it is referenced by LLM profile(s): ...), so arrange the state directly:
  2. Arrange
    control-openhands api POST /api/profiles/QA_broken --write --data '{"llm":{"model":"deepseek/deepseek-chat","provider_connection_id":"00000000000000000000000000000000"},"include_secrets":false}'
  3. Do
    control-openhands browser reload
  4. Check
    control-openhands browser text '[data-testid=profile-row]:has([title="QA_broken"])'

    reads QA_broken deepseek/deepseek-chat Broken link;

  5. Check
    control-openhands browser attr '[data-testid=profile-row]:has([title="QA_broken"]) >> testid=profile-broken-connection-badge' title

    is The linked provider connection was deleted. Edit this profile to re-link it. (screenshot --feature F10.broken-link --name badge).

  6. Note
    Edit it: browser value 'testid=llm-provider-connection-input' is the orphaned id and the listbox offers None, QA_ds_conn and that id.
  7. Expect
    The row's group header shows the raw id, not a name (see Gotchas).

ChatGPT subscription, pre-auth #

  1. Note
    In a fresh editor run
  2. Do
    control-openhands browser click 'testid=sdk-section-basic-toggle'
  3. Note
    first (after the schema step the Default profile has a temperature, so a new editor opens on All; see F10.create-prefill), then
  4. Do
    control-openhands browser click 'testid=llm-auth-type-input'

    (options API key, ChatGPT subscription) and

  5. Do
    control-openhands browser click 'role=option[name="ChatGPT subscription"]'
  6. Note
    browser text 'testid=subscription-status' is ChatGPT subscription not connected, browser value 'testid=llm-subscription-model-input' is gpt-5.5 (list: gpt-5.5, gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-6-astra), and llm-provider-input and llm-api-key-input are gone (count 0).
  7. Do
    control-openhands browser click 'testid=subscription-connect'
  8. Wait
    control-openhands browser wait 'testid=subscription-user-code'
  9. Note
    : the card shows the code, Open sign-in page, I've finished signing in (subscription-poll) and Cancel, and
  10. Do
    control-openhands browser tabs

    lists a second page on auth.openai.com (it opens a second or two after the code appears; rerun browser tabs if it lists one page).

  11. Expect
    The card polls on its own, so Sign-in is not complete yet. ... can already show before you click poll.
  12. Note
    Close it with
  13. Do
    control-openhands browser close-tab 1
  14. Do
    control-openhands browser tab 0
  15. Do
    control-openhands browser click 'testid=subscription-poll'
  16. Note
    leaves Sign-in is not complete yet. Finish the browser flow and try again. on the card, and
  17. Do
    control-openhands browser click 'testid=subscription-cancel'
  18. Note
    removes the code (count 'testid=subscription-user-code' 0).
  19. Note
    Switching back with role=option[name="API key"] restores provider OpenAI and model gpt-5.6-sol.
  20. Note
    Completing sign-in is blocked (needs a ChatGPT Plus/Pro account).

Delete #

  1. Do
    control-openhands browser click '[data-testid=profile-row]:has([title="QA_pro-copy"]) >> testid=profile-menu-trigger'
  2. Do
    control-openhands browser click 'testid=profile-actions-menu >> testid=profile-delete'
  3. Check
    control-openhands browser snapshot 'role=dialog'

    shows Delete Profile with Are you sure you want to delete the profile "QA_pro-copy"? This action cannot be undone., and browser eval "document.activeElement.innerText" is Cancel.

  4. Do
    control-openhands browser click 'role=dialog[name="Delete Profile"] >> role=button[name="Cancel"]'
  5. Note
    keeps the row (count 1).
  6. Note
    Repeat, then
  7. Do
    control-openhands browser click 'testid=delete-profile-confirm'
  8. Wait
    control-openhands browser wait-text 'Profile "QA_pro-copy" deleted'
  9. Note
    reload, and the count is 0.
  10. Note
    Deleting the Default row the same way leaves exactly one profile-active-badge on another profile (browser text '[data-testid=profile-row]:has([data-testid=profile-active-badge])'), and api GET /api/profiles names it in active_profile.

Command menu and direct URL #

  1. Do
    control-openhands browser goto /

    (the bullets above end on /settings/llm, where the URL wait would pass at once),

  2. Do
    control-openhands browser press Control+k
  3. Do
    control-openhands browser type 'testid=command-menu >> role=combobox' LLM
  4. Check
    control-openhands browser snapshot 'testid=command-menu'
  5. Note
    : two options, LLM profiles ("Manage models, providers, and API keys.") and Model Router (F12; its keywords include "llm").
  6. Note
    Hovering an option selects it, so which one Enter runs depends on where the mouse rests: click the option instead,
  7. Do
    control-openhands browser click 'testid=command-menu >> role=option[name="LLM profiles"]' --expect-url '/settings/llm(\?|$)'
  8. Note
    browser count 'testid=add-llm-profile' is 1 on arrival.
  9. Do
    control-openhands browser goto /settings/llm

    shows the same list (browser count 'testid=profile-row', one profile-active-badge; screenshot --feature F10.list --name list).

LLM settings lists deepseek-flash as Default and deepseek-pro as a second profile, with model names and row menus.
Profiles keep reusable model configurations and a default for new conversations. CLI capture · 1440 × 1000 · 9 October 2026 · Canvas 8793c111

LLM settings lists deepseek-flash as Default and deepseek-pro as a second profile, with model names and row menus.

Representative documentation capture, not a complete run of this recipe or family. Captured on an isolated local backend at desktop viewport using the current main checkout.

How this screenshot was taken

canvas: 1.26.0 · agent server: 1.53.0 · sdk: 1.53.0 · automation: 1.19.0

OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser goto /settings/llm
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser wait testid=profile-row
OH_VERIFY_RUN="$OH_VERIFY_RUN" control-openhands browser screenshot --feature F10.list --name llm-profiles

Phone layout #

  1. Do
    control-openhands browser viewport phone
  2. Do
    control-openhands browser goto /settings/llm
  3. Check
    control-openhands browser bbox 'testid=add-llm-profile'
  4. Check
    control-openhands browser bbox 'testid=profile-row >> nth=0'
  5. Do
    control-openhands browser screenshot --feature F10.phone --name list
  6. Note
    : insideViewport true, pageHorizontalOverflow false.
  7. Note
    Open a row menu: browser bbox 'testid=profile-actions-menu' is inside the viewport.
  8. Note
    Open Edit: browser bbox 'testid=profile-name-input' is 348 px wide inside the 390 px viewport and pageHorizontalOverflow is false (llm-settings-screen may report insideViewport false when the edited profile's form is taller than the screen; only the overflow flag matters).
  9. Note
    Return with
  10. Do
    control-openhands browser click 'testid=cancel-profile-btn'
  11. Do
    control-openhands browser viewport desktop

Default profile drives a conversation #

  1. Note
    Make a profile with a distinct model Default through the row menu (for example QA_pro-copy-1, deepseek/deepseek-v4-pro, then wait-text 'Switched to profile "QA_pro-copy-1"'), and run
  2. Wait
    control-openhands conversation start --prompt "Reply with exactly the word: pong" --wait --timeout 180
  3. Expect
    The JSON has "status": "finished" and "model": "deepseek/deepseek-v4-pro".
  4. Expect
    The browser is now on the conversation page:
  5. Do
    control-openhands browser goto /settings/llm
  6. Note
    before driving this family again.

Persistence across a restart #

  1. Do
    control-openhands restart
  2. Check
    control-openhands doctor
  3. Do
    control-openhands browser goto /settings/llm
  4. Note
    : the same rows and the same Default badge are back.

Load error (F10.list-states, blocked) #

  1. Do
    control-openhands service stop agent-server
  2. Do
    control-openhands browser reload
  3. Note
    do not reach testid=profiles-load-error: the whole app is replaced by the disconnected-backend screen (testid=agent-server-onboarding-screen, toasts An error occurred (twice, one after the other) and Could not connect to the configured agent server. Make sure it is running and reachable, then reload the page. (#18051); the Manage backends row detail still reads HTTP request failed (502 Bad Gateway): "Bad Gateway: connect ECONNREFUSED 127.0.0.1:<agent-server port>").
  4. Expect
    The list error needs /api/profiles alone to fail.
  5. Note
    Bring the stack back with
  6. Do
    control-openhands restart

Cleanup #

  1. Do
    control-openhands browser goto /settings/llm

    (the Load error bullet left the disconnected-backend screen), then delete every QA_*, deepseek-chat and deepseek-v3 profile through the row menu, linked ones first;

  2. Check
    control-openhands browser count 'testid=profile-group-header'
  3. Note
    drops to 0 once no linked row is left (F10.list-grouped).
  4. Note
    Then delete QA_ds_conn (testid=provider-connection-row >> has-text=QA_ds_conn >> testid=provider-connection-menu-trigger, testid=provider-connection-actions-menu >> testid=provider-connection-delete, testid=delete-provider-connection-confirm; toast Connection "QA_ds_conn" deleted).
  5. Note
    With no profiles left, testid=profiles-empty returns and api GET /api/profiles is {"profiles":[],"active_profile":null}.

Gotchas and known limits

  • Save always runs a real completion against the provider (except for connection-linked profiles). A bad or missing key fails fast with the provider's message; budget one tiny call per Save.
  • The editor's starting tab is inferred from the values it loads. Edit opens on whichever tab the profile's non-default values need (QA_renamed and QA_adv opened on Basic, QA_broken on All). A new editor opens on Basic only while the agent's LLM settings are all defaults: once the Default profile has a temperature (the schema step) it opens on All, and this survives deleting every profile, because the agent settings keep the last applied values. Click the tab you need before touching its fields, or llm-provider-input / llm-custom-model-input / llm-api-key-help-anchor will not exist.
  • control-openhands service stop agent-server cannot show this page's error states: the whole app switches to the disconnected-backend screen.
  • In Edit, llm-api-key-input holds the stored encrypted key token (about 140 masked characters), not an empty field. Never run browser value on it: it prints the token. Leave it untouched to keep the key.
  • An open provider or model listbox intercepts clicks on the editor's buttons (cancel-profile-btn, save-profile-btn time out with "subtree intercepts pointer events"): browser press Escape first.
  • Provider and model options render in a portal outside the editor and modals: select testid=provider-item-<name> unscoped. Only verified providers have provider-item-* IDs and only Other models have model-item-* IDs; use role=option[name="..."][exact] for the rest. browser testids --filter model-item lists only items scrolled into view; browser count finds the rest.
  • The auto-filled Profile Name never reuses an existing profile's name: picking a model whose name is already a profile's (for example deepseek-flash after llm preset deepseek) empties the field, which disables Save. Recipes fill profile-name-input with a QA_* name anyway.
  • A duplicate profile name disables Save silently: no message, and aria-invalid stays false (#17938). Check browser enabled 'testid=save-profile-btn', not the field styling.
  • The Rename modal does not check duplicates client-side (Rename stays enabled); the server's 409 comes back as the toast Profile '<name>' already exists, and browser errors --app-only counts one http-error and one console.error for it.
  • Set as default (and deleting the Default) rewrites the agent's active LLM settings (control-openhands llm show). Deleting every profile leaves the last applied model and key in agent settings.
  • The app never leaves profiles without a Default: src/routes/root-layout.tsx runs useEnsureActiveProfile (src/hooks/use-ensure-active-profile.ts), which activates the first profile with a key (else the first profile) whenever profiles exist but none is Default, whether never activated or because the Default was deleted (this is what promotes a profile in F10.delete-default). So llm set --no-activate keeps the Default only while another profile is Default: on a run whose only profile is the arranged one, control-openhands api GET /api/profiles --pick active_profile flips from null to it at the next page load (browser reload) and the agent's LLM settings follow (llm show).
  • Profiles created by Add models are named from the model (deepseek-chat, …), not QA_*. The chooser hides a model when a profile with that name exists, not when a profile uses that model. Clean them up before the connection: the server refuses to delete a connection while profiles reference it.
  • A profile linked to a missing connection is grouped under a header showing the raw connection id (00000000…) instead of a name.
  • The All tab logs many [i18n] Missing translation for key "SCHEMA$LLM$..." console warnings; they are not page errors.
  • The list follows the server's order, which sorts by profile file name: QA_pro-copy-1 lists before QA_pro-copy (- sorts before .json's .). Do not assert a strict alphabetical order on names that are prefixes of each other.
  • Toasts last only a few seconds. A browser screenshot taken after a slow command can miss them, so assert with browser wait-text or browser toasts.
  • Known issue #17955: a new profile is saved without the inherited LLM options the editor shows, such as temperature (F10.create-prefill).

Source paths: src/routes/llm-settings.tsx, src/components/features/settings/llm-profiles/, src/components/shared/modals/settings/model-selector.tsx, src/components/features/settings/llm-settings/openai-subscription-auth-card.tsx, src/components/features/settings/sdk-settings/, src/api/profiles-service/profiles-service.api.ts.