Skip to content

[Bug]: model selector resolves to the wrong record when two models share model_name (other record's API key is used) #3007

Description

@tsugumi42

Summary

With two configured models that share the same model_name but belong to different provider instances (different API keys), selecting one of them in the composer model selector resolves to the other one: the request is sent with the other record's API key, and the selector snaps back to that other entry.

Expected: the record the user selects is the one used, together with its own credentials.

Area

Web UI

Reproduction or evidence

Configuration shape — two enabled records for the same upstream model, each with its own key:

# id name model_name provider_instance_id api_key
1 model-x-2 Vendor (account 2) model-x acct-2 key-2
2 model-x Vendor (account 1) model-x acct-1 key-1

Steps

  1. Configure the two records as above.
  2. Select Vendor (account 1) in the composer model selector. The persisted selector ID is model-x (record 2's id).
  3. Send a message.

Observed: the request uses key-2 / acct-2 (record 1). Expected: key-1 / acct-1 (record 2).

Root cause — findSelectableModel in src/web-ui/src/flow_chat/utils/modelResolution.ts:

function findSelectableModel(models: readonly AIModelConfig[], modelRef: string | null | undefined): AIModelConfig | null {
  const value = modelRef?.trim();
  if (!value) return null;
  return models.find(model => isSelectableTextChatModel(model)
    && (model.id === value || model.name === value || model.model_name === value)
  ) ?? null;
}

The predicate accepts a match on any of id / name / model_name, and Array.find returns the first match in array order. Record 2's selector ID (model-x) is also record 1's model_name, so record 1 matches first and wins.

Minimal reproduction — standalone Node script, predicate reproduced as-is (capabilities inlined as the general_chat default):

const isSelectable = m => Boolean(m.id?.trim()) && m.enabled === true && (m.capabilities ?? []).includes('text_chat');
const findSelectableModel = (models, ref) => {
  const value = ref?.trim();
  return models.find(m => isSelectable(m)
    && (m.id === value || m.name === value || m.model_name === value)) ?? null;
};

const models = [
  { id: 'model-x-2', name: 'Vendor (account 2)', model_name: 'model-x', provider_instance_id: 'acct-2', api_key: 'key-2', enabled: true, capabilities: ['text_chat'] },
  { id: 'model-x',   name: 'Vendor (account 1)', model_name: 'model-x', provider_instance_id: 'acct-1', api_key: 'key-1', enabled: true, capabilities: ['text_chat'] },
];

const resolved = findSelectableModel(models, 'model-x');
console.log(resolved.name, resolved.provider_instance_id, resolved.api_key);
// -> Vendor (account 2) acct-2 key-2     (expected: Vendor (account 1) acct-1 key-1)

Second reachable path — the same helper derives the persisted selector ID as model.id?.trim() || model.model_name.trim(). id is optional on AIModelConfig, so a record without id persists its model_name as the selector reference, which is ambiguous by construction once two records share model_name.

Suggested fix direction — match id first and fall back to name / model_name only when nothing matched by id; when several records share model_name, disambiguate with provider_instance_id (already stored in metadata) instead of taking the first array match.

Environment

  • OpenBitFun: 1.0.0-beta desktop; source read at e8e50f56
  • OS: Windows 11
  • Model/provider: a single provider configured twice for the same upstream model (two API keys)

AI-assisted analysis. Testing: lightly tested — root cause read from source and reproduced with the standalone script above; not exercised in a clean build.

Activity

  1. self-assigned this
    on Sep 16, 2026
  2. added a commit that references this issue on Sep 16, 2026
    c448961
  3. added a commit that references this issue on Sep 16, 2026
    339414b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions