mirror of
https://github.com/block/buzz.git
synced 2026-11-11 20:01:09 +00:00
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>
197 lines
6.2 KiB
TypeScript
197 lines
6.2 KiB
TypeScript
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,
|
|
getProviderApiKeyEnvVar,
|
|
shouldClearKnownModelForSelectionScope,
|
|
} from "./agentConfigOptions";
|
|
import { shouldClearModelForRuntimeChange } from "./personaRuntimeModel";
|
|
import {
|
|
envVarsClearingManagedApiKey,
|
|
envVarsWithoutKey,
|
|
} from "./providerEnvVarUpdates";
|
|
|
|
/**
|
|
* Pure transition functions for the runtime -> LLM provider -> model dropdown
|
|
* state machine shared by the persona / create-agent / edit-agent dialogs.
|
|
* Each dialog applies the returned state to its own setters and layers its
|
|
* dialog-specific side effects (inherit pins, command sync, catalog memory)
|
|
* at the call site. Divergent behaviors are parameterized, never merged.
|
|
*/
|
|
export type RuntimeModelProviderSelection = {
|
|
provider: string;
|
|
model: string;
|
|
isCustomProviderEditing: boolean;
|
|
isCustomModelEditing: boolean;
|
|
envVars: EnvVarsValue;
|
|
};
|
|
|
|
export function selectionOnRuntimeChange(
|
|
current: RuntimeModelProviderSelection,
|
|
params: {
|
|
previousRuntime: string;
|
|
nextRuntime: string;
|
|
/** Caller-computed: whether the next runtime supports provider selection. */
|
|
nextRuntimeCanChooseProvider: boolean;
|
|
/**
|
|
* Persona/Edit clear the managed API key and custom-model editing flag
|
|
* when switching to a provider-locked runtime ("full"); Create clears
|
|
* only the provider selection ("provider-only").
|
|
*/
|
|
lockedRuntimeReset: "full" | "provider-only";
|
|
},
|
|
): RuntimeModelProviderSelection {
|
|
const next = { ...current };
|
|
|
|
if (
|
|
shouldClearModelForRuntimeChange(
|
|
params.previousRuntime,
|
|
params.nextRuntime,
|
|
) ||
|
|
shouldClearKnownModelForSelectionScope({
|
|
model: current.model,
|
|
provider: current.provider,
|
|
runtime: params.nextRuntime,
|
|
})
|
|
) {
|
|
next.model = "";
|
|
next.isCustomModelEditing = false;
|
|
}
|
|
|
|
if (!params.nextRuntimeCanChooseProvider) {
|
|
if (params.lockedRuntimeReset === "full") {
|
|
next.envVars = envVarsClearingManagedApiKey(
|
|
next.envVars,
|
|
current.provider,
|
|
"",
|
|
);
|
|
next.isCustomModelEditing = false;
|
|
}
|
|
next.isCustomProviderEditing = false;
|
|
next.provider = "";
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function selectionOnProviderDropdownChange(
|
|
current: RuntimeModelProviderSelection,
|
|
params: {
|
|
/** Runtime id used for the model-scope clearing rule. */
|
|
runtime: string;
|
|
nextValue: string;
|
|
/**
|
|
* Persona-only: clear the model when the newly selected provider's API
|
|
* key is not yet filled (model discovery cannot run without it).
|
|
*/
|
|
clearModelWhenApiKeyMissing: boolean;
|
|
},
|
|
): RuntimeModelProviderSelection {
|
|
const next = { ...current };
|
|
|
|
if (params.nextValue === CUSTOM_PROVIDER_DROPDOWN_VALUE) {
|
|
const previousEnvVar = getProviderApiKeyEnvVar(current.provider);
|
|
if (previousEnvVar) {
|
|
next.envVars = envVarsWithoutKey(next.envVars, previousEnvVar);
|
|
}
|
|
next.isCustomProviderEditing = true;
|
|
next.provider = "";
|
|
return next;
|
|
}
|
|
|
|
const nextProvider =
|
|
params.nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : params.nextValue;
|
|
next.envVars = envVarsClearingManagedApiKey(
|
|
next.envVars,
|
|
current.provider,
|
|
nextProvider,
|
|
);
|
|
next.isCustomProviderEditing = false;
|
|
next.provider = nextProvider;
|
|
|
|
if (params.clearModelWhenApiKeyMissing) {
|
|
const requiredEnvVar = getProviderApiKeyEnvVar(nextProvider);
|
|
if (requiredEnvVar && !next.envVars[requiredEnvVar]?.trim()) {
|
|
next.model = "";
|
|
next.isCustomModelEditing = false;
|
|
}
|
|
}
|
|
|
|
// Guard on the PRE-transition editing flag, matching all three dialogs
|
|
// (their handlers read the render-scope value).
|
|
if (
|
|
!current.isCustomModelEditing &&
|
|
shouldClearKnownModelForSelectionScope({
|
|
model: current.model,
|
|
provider: nextProvider,
|
|
runtime: params.runtime,
|
|
})
|
|
) {
|
|
next.model = "";
|
|
next.isCustomModelEditing = false;
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function selectionOnModelDropdownChange(
|
|
current: RuntimeModelProviderSelection,
|
|
params: {
|
|
nextValue: string;
|
|
/**
|
|
* Persona clears a known (non-custom) model when entering custom mode;
|
|
* Create/Edit keep it as the editable starting value.
|
|
*/
|
|
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 };
|
|
|
|
if (params.nextValue === CUSTOM_MODEL_DROPDOWN_VALUE) {
|
|
next.isCustomModelEditing = true;
|
|
if (params.clearKnownModelOnCustomEntry && !params.isModelCustom) {
|
|
next.model = "";
|
|
}
|
|
return next;
|
|
}
|
|
|
|
const isRelayMesh =
|
|
params.isRelayMesh ?? current.provider.trim() === RELAY_MESH_PROVIDER;
|
|
|
|
next.isCustomModelEditing = false;
|
|
next.model =
|
|
params.nextValue === AUTO_MODEL_DROPDOWN_VALUE
|
|
? isRelayMesh
|
|
? RELAY_MESH_AUTO_MODEL
|
|
: ""
|
|
: params.nextValue;
|
|
return next;
|
|
}
|