/** * 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; /** 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"; }