mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
307 lines
11 KiB
TypeScript
307 lines
11 KiB
TypeScript
/**
|
|
* Source-of-truth constants for buzz-agent model-tuning configuration knobs.
|
|
*
|
|
* Values must stay in sync with `crates/buzz-agent/src/config.rs`
|
|
* `parse_thinking_effort` — that function is the authoritative list.
|
|
*/
|
|
|
|
/** Env var key for the thinking/effort level sent to the LLM. */
|
|
export const BUZZ_AGENT_THINKING_EFFORT = "BUZZ_AGENT_THINKING_EFFORT";
|
|
|
|
/** Env var key for the maximum output token count per turn. */
|
|
export const BUZZ_AGENT_MAX_OUTPUT_TOKENS = "BUZZ_AGENT_MAX_OUTPUT_TOKENS";
|
|
|
|
/** Env var key for the context window token limit. */
|
|
export const BUZZ_AGENT_MAX_CONTEXT_TOKENS = "BUZZ_AGENT_MAX_CONTEXT_TOKENS";
|
|
|
|
/** Env var key for the maximum number of LLM/tool rounds per turn. */
|
|
export const BUZZ_AGENT_MAX_ROUNDS = "BUZZ_AGENT_MAX_ROUNDS";
|
|
|
|
/**
|
|
* Ordered set of valid thinking-effort values accepted by buzz-agent.
|
|
* Mirrors `parse_thinking_effort` in `crates/buzz-agent/src/config.rs`.
|
|
*/
|
|
export const BUZZ_AGENT_THINKING_EFFORT_VALUES = [
|
|
"none",
|
|
"minimal",
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
"max",
|
|
] as const;
|
|
|
|
export type ThinkingEffortValue =
|
|
(typeof BUZZ_AGENT_THINKING_EFFORT_VALUES)[number];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider-aware effort configuration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Describes which thinking-effort values are valid for a given provider+model,
|
|
* and which value is the provider's semantic default.
|
|
*
|
|
* `defaultValue = null` means the provider/model's default is to omit the
|
|
* thinking configuration entirely (i.e. "Inherit" is the natural default).
|
|
* This applies to Anthropic manual-budget models where the effort level maps
|
|
* to a budget_tokens count — there is no "default effort level" in the API.
|
|
*
|
|
* Mirrors the model-family tables in `crates/buzz-agent/src/config.rs`
|
|
* (`openai_efforts_for_model`, `is_manual_budget_model`,
|
|
* `is_adaptive_thinking_model`, `clamp_adaptive_effort`). Keep in sync.
|
|
*/
|
|
export type ProviderEffortConfig = {
|
|
validValues: ReadonlyArray<ThinkingEffortValue>;
|
|
/** Provider/model's semantic default, or `null` when Inherit is the default. */
|
|
defaultValue: ThinkingEffortValue | null;
|
|
};
|
|
|
|
const ALL_VALUES = BUZZ_AGENT_THINKING_EFFORT_VALUES;
|
|
|
|
/**
|
|
* Returns the valid thinking-effort values and semantic default for the
|
|
* given provider and optional model string.
|
|
*
|
|
* Model matching mirrors the Rust backend:
|
|
* - Anthropic: strip any endpoint-naming prefix, then test `is_manual_budget_model`
|
|
* / `is_adaptive_thinking_model` / `clamp_adaptive_effort` family checks.
|
|
* - OpenAI: strip any endpoint-naming prefix, then test `openai_efforts_for_model`
|
|
* family checks (boundary-aware: -pro before -5.x, digit/letter boundary).
|
|
* - DatabricksV2: strip prefix and route by model family.
|
|
* - Unknown/empty: all 7 values, default medium.
|
|
*
|
|
* Prefix stripping: finds the first occurrence of a known model-family token
|
|
* (`claude-`, `gpt-`) and drops everything before it. This handles any
|
|
* endpoint-naming convention (e.g. `databricks-`, `goose-`, `team-x-`) without
|
|
* maintaining an allowlist of known prefixes. If no family token is found, the
|
|
* raw model name is used as-is.
|
|
*/
|
|
export function getProviderEffortConfig(
|
|
providerId: string,
|
|
model?: string,
|
|
): ProviderEffortConfig {
|
|
const provider = providerId.toLowerCase();
|
|
// Strip arbitrary endpoint-naming prefix before model-family matching.
|
|
// Find the first occurrence of a known family token and drop everything before it.
|
|
// e.g. "goose-claude-fable-5" → "claude-fable-5"
|
|
// "team-x-gpt-5.5" → "gpt-5.5"
|
|
// "databricks-claude-3" → "claude-3"
|
|
// "claude-opus-4-7" → "claude-opus-4-7" (no prefix to strip)
|
|
const rawModel = (model ?? "").trim().toLowerCase();
|
|
const FAMILY_TOKENS = ["claude-", "gpt-"] as const;
|
|
const firstFamilyIdx = Math.min(
|
|
...FAMILY_TOKENS.map((tok) => {
|
|
const idx = rawModel.indexOf(tok);
|
|
return idx === -1 ? Infinity : idx;
|
|
}),
|
|
);
|
|
const m =
|
|
firstFamilyIdx === Infinity ? rawModel : rawModel.slice(firstFamilyIdx);
|
|
|
|
if (provider === "anthropic") {
|
|
return anthropicConfig(m);
|
|
}
|
|
if (provider === "openai") {
|
|
return openaiConfig(m);
|
|
}
|
|
if (provider === "databricks_v2") {
|
|
// Route by model family: claude* → Anthropic tables, gpt-5* → OpenAI tables.
|
|
// Non-Claude concrete models (e.g. llama-3) go through MlflowChatCompletions,
|
|
// which applies normalize_effort_for_openai_route → clamps max to xhigh.
|
|
// Route them through openaiConfig to exclude max. Only blank/unknown model
|
|
// uses the all-7 fallback (can't know the route without a concrete model).
|
|
if (m.startsWith("claude-")) {
|
|
return anthropicConfig(m);
|
|
}
|
|
if (gpt5FamilyModel(m)) {
|
|
return openaiConfig(m);
|
|
}
|
|
if (m.length > 0) {
|
|
// Concrete non-Claude, non-GPT model → MLflow path clamps max → xhigh.
|
|
return openaiConfig(m);
|
|
}
|
|
// Blank model — route unknown, show all 7.
|
|
return { validValues: ALL_VALUES, defaultValue: "medium" };
|
|
}
|
|
if (provider === "databricks") {
|
|
// databricks v1 uses OpenAI Chat Completions wire format.
|
|
return openaiConfig(m);
|
|
}
|
|
// openai-compat, unknown, empty — all values, default medium.
|
|
return { validValues: ALL_VALUES, defaultValue: "medium" };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Anthropic family tables
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function anthropicConfig(m: string): ProviderEffortConfig {
|
|
// Manual-budget models: claude-3* and claude-opus-4-5.
|
|
// These use budget_tokens — there is no "default effort level" in the API.
|
|
if (m.startsWith("claude-3") || m === "claude-opus-4-5") {
|
|
return {
|
|
validValues: ["low", "medium", "high"],
|
|
defaultValue: null,
|
|
};
|
|
}
|
|
// Adaptive models that support xhigh: opus-4-7+, sonnet-5.x, fable-5, mythos-5.
|
|
// mirrors clamp_adaptive_effort supports_xhigh check.
|
|
if (
|
|
m.startsWith("claude-opus-4-7") ||
|
|
m.startsWith("claude-opus-4-8") ||
|
|
m.startsWith("claude-sonnet-5") ||
|
|
m.startsWith("claude-fable-5") ||
|
|
m.startsWith("claude-mythos-5")
|
|
) {
|
|
return {
|
|
validValues: ["low", "medium", "high", "xhigh", "max"],
|
|
defaultValue: "high",
|
|
};
|
|
}
|
|
// Adaptive models that do NOT support xhigh: opus-4-6, sonnet-4-6, mythos-preview.
|
|
if (
|
|
m.startsWith("claude-opus-4-6") ||
|
|
m.startsWith("claude-sonnet-4-6") ||
|
|
m.startsWith("claude-mythos-preview")
|
|
) {
|
|
return {
|
|
validValues: ["low", "medium", "high", "max"],
|
|
defaultValue: "high",
|
|
};
|
|
}
|
|
// Unknown Anthropic model — assume adaptive with full support.
|
|
return {
|
|
validValues: ["low", "medium", "high", "xhigh", "max"],
|
|
defaultValue: "high",
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OpenAI family tables — mirrors openai_efforts_for_model in config.rs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns true if `m` contains a GPT-5 family token at a word boundary
|
|
* (not immediately followed by a digit or letter). Mirrors
|
|
* `gpt5_token_matches` / `gpt5_base_matches` in config.rs.
|
|
*/
|
|
function gpt5TokenMatches(m: string, token: string): boolean {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = m.indexOf(token, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + token.length;
|
|
const afterChar = afterIdx < m.length ? m[afterIdx] : "";
|
|
// Boundary: end-of-string or a `-` separator (not a digit or letter).
|
|
if (afterChar === "" || afterChar === "-") return true;
|
|
start = afterIdx;
|
|
}
|
|
}
|
|
|
|
/** Like gpt5TokenMatches but also rejects short -<1-3 digit> suffixes (e.g. -5, -10). */
|
|
function gpt5BaseMatches(m: string, token: string): boolean {
|
|
let start = 0;
|
|
while (true) {
|
|
const idx = m.indexOf(token, start);
|
|
if (idx === -1) return false;
|
|
const afterIdx = idx + token.length;
|
|
const suffix = m.slice(afterIdx);
|
|
if (suffix === "") return true;
|
|
if (!suffix.startsWith("-")) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
// Has a `-` suffix — check if it looks like a 1-3 digit version number.
|
|
const dashRest = suffix.slice(1);
|
|
if (/^\d{1,3}(?:[^a-z\d]|$)/i.test(dashRest)) {
|
|
start = afterIdx;
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** Returns true if the model string belongs to any GPT-5 family. */
|
|
function gpt5FamilyModel(m: string): boolean {
|
|
return (
|
|
gpt5TokenMatches(m, "gpt-5-pro") ||
|
|
gpt5TokenMatches(m, "gpt5-pro") ||
|
|
gpt5TokenMatches(m, "gpt-5.6") ||
|
|
gpt5TokenMatches(m, "gpt5.6") ||
|
|
gpt5TokenMatches(m, "gpt-5-6") ||
|
|
gpt5TokenMatches(m, "gpt5-6") ||
|
|
gpt5TokenMatches(m, "gpt-5.5") ||
|
|
gpt5TokenMatches(m, "gpt5.5") ||
|
|
gpt5TokenMatches(m, "gpt-5.4") ||
|
|
gpt5TokenMatches(m, "gpt5.4") ||
|
|
gpt5TokenMatches(m, "gpt-5.1") ||
|
|
gpt5TokenMatches(m, "gpt5.1") ||
|
|
gpt5BaseMatches(m, "gpt-5") ||
|
|
gpt5BaseMatches(m, "gpt5")
|
|
);
|
|
}
|
|
|
|
function openaiConfig(m: string): ProviderEffortConfig {
|
|
// Check -pro before versioned suffixes (gpt-5-pro contains "gpt-5").
|
|
if (gpt5TokenMatches(m, "gpt-5-pro") || gpt5TokenMatches(m, "gpt5-pro")) {
|
|
return { validValues: ["high"], defaultValue: "high" };
|
|
}
|
|
if (
|
|
gpt5TokenMatches(m, "gpt-5.6") ||
|
|
gpt5TokenMatches(m, "gpt5.6") ||
|
|
gpt5TokenMatches(m, "gpt-5-6") ||
|
|
gpt5TokenMatches(m, "gpt5-6")
|
|
) {
|
|
return {
|
|
validValues: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
defaultValue: "medium",
|
|
};
|
|
}
|
|
if (
|
|
gpt5TokenMatches(m, "gpt-5.5") ||
|
|
gpt5TokenMatches(m, "gpt5.5") ||
|
|
gpt5TokenMatches(m, "gpt-5-5") ||
|
|
gpt5TokenMatches(m, "gpt5-5") ||
|
|
gpt5TokenMatches(m, "gpt-5.4") ||
|
|
gpt5TokenMatches(m, "gpt5.4") ||
|
|
gpt5TokenMatches(m, "gpt-5-4") ||
|
|
gpt5TokenMatches(m, "gpt5-4")
|
|
) {
|
|
return {
|
|
validValues: ["none", "low", "medium", "high", "xhigh"],
|
|
defaultValue: "medium",
|
|
};
|
|
}
|
|
if (
|
|
gpt5TokenMatches(m, "gpt-5.1") ||
|
|
gpt5TokenMatches(m, "gpt5.1") ||
|
|
gpt5TokenMatches(m, "gpt-5-1") ||
|
|
gpt5TokenMatches(m, "gpt5-1")
|
|
) {
|
|
return {
|
|
validValues: ["none", "low", "medium", "high"],
|
|
defaultValue: "none",
|
|
};
|
|
}
|
|
if (gpt5BaseMatches(m, "gpt-5") || gpt5BaseMatches(m, "gpt5")) {
|
|
return {
|
|
validValues: ["minimal", "low", "medium", "high"],
|
|
defaultValue: "medium",
|
|
};
|
|
}
|
|
// Unknown OpenAI model — conservative fallback; max is enabled only for families whose table includes it.
|
|
return {
|
|
validValues: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
defaultValue: "medium",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns true when the given runtime id is buzz-agent, which is the only
|
|
* runtime that supports the tier-1 model-tuning knobs above.
|
|
*/
|
|
export function isBuzzAgentRuntime(runtimeId: string): boolean {
|
|
return runtimeId === "buzz-agent";
|
|
}
|