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
- Configure the two records as above.
- Select Vendor (account 1) in the composer model selector. The persisted selector ID is
model-x (record 2's id).
- 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.
Summary
With two configured models that share the same
model_namebut 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:
idnamemodel_nameprovider_instance_idapi_keymodel-x-2model-xacct-2key-2model-xmodel-xacct-1key-1Steps
model-x(record 2'sid).Observed: the request uses
key-2/acct-2(record 1). Expected:key-1/acct-1(record 2).Root cause —
findSelectableModelinsrc/web-ui/src/flow_chat/utils/modelResolution.ts:The predicate accepts a match on any of
id/name/model_name, andArray.findreturns the first match in array order. Record 2's selector ID (model-x) is also record 1'smodel_name, so record 1 matches first and wins.Minimal reproduction — standalone Node script, predicate reproduced as-is (capabilities inlined as the
general_chatdefault):Second reachable path — the same helper derives the persisted selector ID as
model.id?.trim() || model.model_name.trim().idis optional onAIModelConfig, so a record withoutidpersists itsmodel_nameas the selector reference, which is ambiguous by construction once two records sharemodel_name.Suggested fix direction — match
idfirst and fall back toname/model_nameonly when nothing matched byid; when several records sharemodel_name, disambiguate withprovider_instance_id(already stored inmetadata) instead of taking the first array match.Environment
1.0.0-betadesktop; source read ate8e50f56AI-assisted analysis. Testing: lightly tested — root cause read from source and reproduced with the standalone script above; not exercised in a clean build.