diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index 5c515a0507..acbeebff97 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -37,6 +37,19 @@ export const ADVANCED_FIELDS_MOTION_TRANSITION = { } as const; export const AUTO_MODEL_DROPDOWN_VALUE = "__auto_model__"; +/** + * The model value that means "Auto" on Buzz shared compute. + * + * Not blank: blank means *inherit the global model*, which for this provider + * resolves to whatever unrelated model the global config names. It is also a + * trigger token rather than a final choice -- buzz-agent rewrites exactly + * `"auto"` to the virtual `mesh` model when the live catalog offers enough + * models for Mixture-of-Agents (`BUZZ_AGENT_PREFER_MESH_FOR_AUTO`), so any + * concrete model id defeats that translation. + */ +export const RELAY_MESH_AUTO_MODEL = "auto"; +/** Provider id for Buzz shared compute. */ +export const RELAY_MESH_PROVIDER = "relay-mesh"; export const CUSTOM_MODEL_DROPDOWN_VALUE = "__custom_model__"; export const AUTO_PROVIDER_DROPDOWN_VALUE = "__auto_provider__"; export const CUSTOM_PROVIDER_DROPDOWN_VALUE = "__custom_provider__"; diff --git a/desktop/src/features/agents/ui/runtimeModelProviderSelection.test.mjs b/desktop/src/features/agents/ui/runtimeModelProviderSelection.test.mjs index 470b2312bc..1488307ef0 100644 --- a/desktop/src/features/agents/ui/runtimeModelProviderSelection.test.mjs +++ b/desktop/src/features/agents/ui/runtimeModelProviderSelection.test.mjs @@ -228,3 +228,94 @@ test("auto-model selection clears the model; concrete selection sets it", () => ); assert.equal(concrete.model, "gpt-5"); }); + +test('shared compute encodes Auto as "auto", not blank', () => { + // Blank means *inherit the global model*, so on shared compute a blank Auto + // resolves to whatever unrelated model the global config names -- a Claude id + // sent to a local mesh node -- and it fails the raw-pair submit gate in + // `agentAiConfigurationModeSatisfied`, which is what greyed out Create. + // + // "auto" is also a trigger token, not a final choice: buzz-agent rewrites + // exactly "auto" to the virtual `mesh` model when the catalog offers enough + // models for Mixture-of-Agents. Any concrete id defeats that translation. + const auto = selectionOnModelDropdownChange( + { ...base, provider: "relay-mesh", model: "some-physical-model" }, + { + nextValue: "__auto_model__", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + isRelayMesh: true, + }, + ); + assert.equal(auto.model, "auto"); + assert.equal(auto.isCustomModelEditing, false); +}); + +test("a concrete shared-compute model is still honoured verbatim", () => { + // Only Auto is special. An explicit pick must reach mesh-llm unchanged. + const concrete = selectionOnModelDropdownChange( + { ...base, provider: "relay-mesh", model: "auto" }, + { + nextValue: "unsloth/gemma-4-26B", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + isRelayMesh: true, + }, + ); + assert.equal(concrete.model, "unsloth/gemma-4-26B"); +}); + +test("ordinary providers keep blank for Auto", () => { + // Blank is the generic "inherit the global default" encoding everywhere else, + // and must not be migrated to "auto". + const auto = selectionOnModelDropdownChange( + { ...base, provider: "anthropic", model: "claude-opus-5" }, + { + nextValue: "__auto_model__", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + isRelayMesh: false, + }, + ); + assert.equal(auto.model, ""); + // Omitting the flag entirely behaves the same, so existing callers are safe. + const omitted = selectionOnModelDropdownChange( + { ...base, provider: "anthropic", model: "claude-opus-5" }, + { + nextValue: "__auto_model__", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + }, + ); + assert.equal(omitted.model, ""); +}); + +test("shared compute is inferred from the selection when the caller omits it", () => { + // The Edit dialog cannot pass the flag (its file is at the size ceiling), so + // the selection's own provider has to be enough for the common case where the + // agent names relay-mesh itself. + const auto = selectionOnModelDropdownChange( + { ...base, provider: "relay-mesh", model: "some-physical-model" }, + { + nextValue: "__auto_model__", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + }, + ); + assert.equal(auto.model, "auto"); +}); + +test("an explicit flag wins over the selection's own provider", () => { + // Inheritance case: the picker can run off an effective provider the selection + // does not name, so a caller that knows better must be able to say so. + const inherited = selectionOnModelDropdownChange( + { ...base, provider: "", model: "x" }, + { + nextValue: "__auto_model__", + clearKnownModelOnCustomEntry: false, + isModelCustom: false, + isRelayMesh: true, + }, + ); + assert.equal(inherited.model, "auto"); +}); diff --git a/desktop/src/features/agents/ui/runtimeModelProviderSelection.ts b/desktop/src/features/agents/ui/runtimeModelProviderSelection.ts index e98dc540df..c360a66f25 100644 --- a/desktop/src/features/agents/ui/runtimeModelProviderSelection.ts +++ b/desktop/src/features/agents/ui/runtimeModelProviderSelection.ts @@ -1,6 +1,8 @@ import type { EnvVarsValue } from "./EnvVarsEditor"; import { AUTO_MODEL_DROPDOWN_VALUE, + RELAY_MESH_AUTO_MODEL, + RELAY_MESH_PROVIDER, AUTO_PROVIDER_DROPDOWN_VALUE, CUSTOM_MODEL_DROPDOWN_VALUE, CUSTOM_PROVIDER_DROPDOWN_VALUE, @@ -147,6 +149,27 @@ export function selectionOnModelDropdownChange( clearKnownModelOnCustomEntry: boolean; /** Caller-computed: whether the current model is outside the known options. */ isModelCustom: boolean; + /** + * Caller-computed: whether the effective provider is Buzz shared compute. + * + * Shared compute encodes Auto as the literal `"auto"`, not as blank. Blank + * means *inherit the global model*, so a blank Auto on shared compute both + * resolves to whatever unrelated model the global config names (e.g. a + * Claude id sent to a local mesh node) and fails the raw-pair submit gate in + * `agentAiConfigurationModeSatisfied`, which requires a non-empty model. + * + * `"auto"` is also a trigger token rather than a final choice: Buzz sets + * `BUZZ_AGENT_PREFER_MESH_FOR_AUTO` for this provider, and buzz-agent + * rewrites exactly `"auto"` to the virtual `mesh` model when the live + * catalog offers enough models for Mixture-of-Agents. Any concrete model id + * defeats that translation. + * + * Optional. Callers that already derive it (because the picker can be driven + * by an *inherited* effective provider, not just the selection's own) should + * pass it; otherwise the selection's own provider is used, which covers every + * case where the agent names the provider itself. + */ + isRelayMesh?: boolean; }, ): RuntimeModelProviderSelection { const next = { ...current }; @@ -159,8 +182,15 @@ export function selectionOnModelDropdownChange( return next; } + const isRelayMesh = + params.isRelayMesh ?? current.provider.trim() === RELAY_MESH_PROVIDER; + next.isCustomModelEditing = false; next.model = - params.nextValue === AUTO_MODEL_DROPDOWN_VALUE ? "" : params.nextValue; + params.nextValue === AUTO_MODEL_DROPDOWN_VALUE + ? isRelayMesh + ? RELAY_MESH_AUTO_MODEL + : "" + : params.nextValue; return next; }