fix(desktop): picking Auto on shared compute no longer blocks Create

Selecting "Auto" for a Buzz shared compute agent left Create/Save greyed
out. The submit gate requires a non-empty model on the raw provider/model
pair (`agentAiConfigurationModeSatisfied`), and the model dropdown wrote
blank for Auto.

Blank was also wrong on its own terms. Blank means *inherit the global
model*, so an agent whose model came out blank ran shared compute against
whatever unrelated model global config named - a Claude id sent to a local
mesh node. And `"auto"` is 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 mesh-llm's virtual `mesh` model
once the catalog advertises it (which mesh-llm does automatically at two
or more models). Any concrete model id defeats that translation, so the
old blank encoding could never reach Mixture-of-Agents.

The rest of the feature already encoded shared-compute Auto as `"auto"`
(provider selection, the global model control); the model dropdown handler
was the one inconsistent path. Ordinary providers keep blank, which is the
generic inheritance encoding everywhere else - existing blanks are not
migrated.

`isRelayMesh` is an optional parameter, defaulting to the selection's own
provider. The dialogs derive the value already and would ideally pass it
for the inherited-provider case, but both files sit at the size ceiling
and may not grow; the parameter is there for whoever splits them.

The existing test enshrined unconditional Auto -> blank. It is now split by
provider, with coverage for the inference fallback and the explicit flag.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-08-06 14:56:21 +10:00
parent ea389a4fb4
commit 5ed5925a51
3 changed files with 135 additions and 1 deletions

View File

@@ -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__";

View File

@@ -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");
});

View File

@@ -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;
}