From a34320783197a4f4e0d7c34c7adc8038f98206ce Mon Sep 17 00:00:00 2001 From: maddygoround Date: Fri, 24 Jul 2026 19:10:55 +0530 Subject: [PATCH 1/3] feat(agent): add OpenAI service tier configuration and UI support Signed-off-by: maddygoround --- crates/buzz-agent/README.md | 1 + crates/buzz-agent/src/config.rs | 60 +++++++++++++++++ crates/buzz-agent/src/llm.rs | 67 +++++++++++++------ .../agents/ui/buzzAgentConfig.test.mjs | 20 ++++++ .../src/features/agents/ui/buzzAgentConfig.ts | 13 ++++ .../agents/ui/buzzAgentModelTuningFields.tsx | 35 ++++++++++ 6 files changed, 175 insertions(+), 21 deletions(-) diff --git a/crates/buzz-agent/README.md b/crates/buzz-agent/README.md index a2504db451..944f7720e0 100644 --- a/crates/buzz-agent/README.md +++ b/crates/buzz-agent/README.md @@ -138,6 +138,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro | `OPENAI_COMPAT_MODEL` | — | Required when provider=openai. | | `OPENAI_COMPAT_BASE_URL` | `https://api.openai.com/v1` | Point at vLLM, llama.cpp, OpenRouter, Ollama, etc. | | `OPENAI_COMPAT_API` | `auto` | `auto` \| `chat` \| `responses`. `auto` picks Responses for `*.openai.com`, Chat Completions everywhere else. | +| `BUZZ_AGENT_SERVICE_TIER` | — | Optional OpenAI processing tier: `auto`, `default`, `flex`, or `priority`. | | `DATABRICKS_HOST` | — | Required when provider=databricks or provider=databricks_v2. | | `DATABRICKS_MODEL` | — | Required when provider=databricks or provider=databricks_v2. | | `DATABRICKS_TOKEN` | — | Optional static bearer escape hatch. If unset, Databricks uses browser OAuth + refresh cache. | diff --git a/crates/buzz-agent/src/config.rs b/crates/buzz-agent/src/config.rs index f06849c7d1..cd56433c8b 100644 --- a/crates/buzz-agent/src/config.rs +++ b/crates/buzz-agent/src/config.rs @@ -683,6 +683,27 @@ pub enum OpenAiApi { Auto, } +/// OpenAI request processing tier. `None` leaves the provider default intact. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpenAiServiceTier { + Auto, + Default, + Flex, + Priority, +} + +impl OpenAiServiceTier { + /// Return the wire-format value expected by OpenAI APIs. + pub fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Default => "default", + Self::Flex => "flex", + Self::Priority => "priority", + } + } +} + #[derive(Debug, Clone)] pub struct Config { pub provider: Provider, @@ -730,6 +751,8 @@ pub struct Config { /// Thinking/reasoning effort level. `None` = use provider default (no /// thinking config sent). Set via `BUZZ_AGENT_THINKING_EFFORT`. pub thinking_effort: Option, + /// OpenAI processing tier. Set via `BUZZ_AGENT_SERVICE_TIER`. + pub service_tier: Option, } impl Config { @@ -824,6 +847,7 @@ impl Config { hook_servers: parse_hook_servers_env("MCP_HOOK_SERVERS"), hints_enabled: parse_env("BUZZ_AGENT_NO_HINTS", 0u8)? == 0, thinking_effort: parse_thinking_effort(env("BUZZ_AGENT_THINKING_EFFORT").as_deref())?, + service_tier: parse_service_tier(env("BUZZ_AGENT_SERVICE_TIER").as_deref())?, }; cfg.validate()?; Ok(cfg) @@ -864,6 +888,7 @@ impl Config { hook_servers: HookServers::None, hints_enabled: false, thinking_effort: None, + service_tier: None, } } @@ -951,6 +976,22 @@ impl Config { } } +/// Parse the optional OpenAI service tier. Empty values preserve provider defaults. +pub fn parse_service_tier(raw: Option<&str>) -> Result, String> { + match raw.map(str::trim).filter(|value| !value.is_empty()) { + None => Ok(None), + Some(value) => match value.to_ascii_lowercase().as_str() { + "auto" => Ok(Some(OpenAiServiceTier::Auto)), + "default" => Ok(Some(OpenAiServiceTier::Default)), + "flex" => Ok(Some(OpenAiServiceTier::Flex)), + "priority" => Ok(Some(OpenAiServiceTier::Priority)), + other => Err(format!( + "config: BUZZ_AGENT_SERVICE_TIER={other} not supported (use auto|default|flex|priority)" + )), + }, + } +} + fn env(k: &str) -> Option { std::env::var(k).ok() } @@ -1347,6 +1388,25 @@ mod tests { ); } + #[test] + fn parse_service_tier_accepts_openai_values_case_insensitively() { + assert_eq!( + parse_service_tier(Some(" FLEX ")).unwrap(), + Some(OpenAiServiceTier::Flex) + ); + assert_eq!( + parse_service_tier(Some("priority")).unwrap(), + Some(OpenAiServiceTier::Priority) + ); + assert_eq!(parse_service_tier(Some(" ")).unwrap(), None); + } + + #[test] + fn parse_service_tier_rejects_unknown_value() { + let err = parse_service_tier(Some("economy")).unwrap_err(); + assert!(err.contains("BUZZ_AGENT_SERVICE_TIER=economy"), "{err}"); + } + #[test] fn thinking_effort_anthropic_budget_tokens_mapping() { assert_eq!(ThinkingEffort::Low.anthropic_budget_tokens(), 1_024); diff --git a/crates/buzz-agent/src/llm.rs b/crates/buzz-agent/src/llm.rs index 758be4e124..ea1c6596c3 100644 --- a/crates/buzz-agent/src/llm.rs +++ b/crates/buzz-agent/src/llm.rs @@ -183,28 +183,26 @@ impl Llm { let r = self .openai_request(cfg, effective_model, |use_responses| { if use_responses { - ( - json!({ - "model": effective_model, - "max_output_tokens": max_output_tokens, - "instructions": system_prompt, - "input": user_prompt, - }), - parse_responses as OpenAiParse, - ) + let mut body = json!({ + "model": effective_model, + "max_output_tokens": max_output_tokens, + "instructions": system_prompt, + "input": user_prompt, + }); + apply_openai_service_tier(cfg, &mut body); + (body, parse_responses as OpenAiParse) } else { - ( - json!({ - "model": effective_model, - "stream": false, - "max_completion_tokens": max_output_tokens, - "messages": [ - { "role": "system", "content": system_prompt }, - { "role": "user", "content": user_prompt }, - ], - }), - parse_openai as OpenAiParse, - ) + let mut body = json!({ + "model": effective_model, + "stream": false, + "max_completion_tokens": max_output_tokens, + "messages": [ + { "role": "system", "content": system_prompt }, + { "role": "user", "content": user_prompt }, + ], + }); + apply_openai_service_tier(cfg, &mut body); + (body, parse_openai as OpenAiParse) } }) .await?; @@ -545,6 +543,7 @@ fn openai_body( if let Some(e) = effort { body["reasoning_effort"] = json!(e.openai_effort_str()); } + apply_openai_service_tier(cfg, &mut body); if !tools_json.is_empty() { body["tools"] = Value::Array(tools_json); body["tool_choice"] = json!("auto"); @@ -664,6 +663,7 @@ fn responses_body( if let Some(e) = effort { body["reasoning"] = json!({ "effort": e.openai_effort_str() }); } + apply_openai_service_tier(cfg, &mut body); if !tools_json.is_empty() { body["tools"] = Value::Array(tools_json); body["tool_choice"] = json!("auto"); @@ -671,6 +671,14 @@ fn responses_body( body } +fn apply_openai_service_tier(cfg: &Config, body: &mut Value) { + if matches!(cfg.provider, Provider::OpenAi) { + if let Some(tier) = cfg.service_tier { + body["service_tier"] = json!(tier.as_str()); + } + } +} + /// Narrow matcher for "you should be on the Responses API" provider errors, /// the signal we use to auto-upgrade. Triggers on the literal path /// `/v1/responses` (Databricks GPT-5.5 phrasing) or the prose @@ -1251,6 +1259,7 @@ mod tests { openai_api: OpenAiApi::Chat, hints_enabled: true, thinking_effort: None, + service_tier: None, } } @@ -1846,6 +1855,22 @@ mod tests { ); } + #[test] + fn openai_body_emits_service_tier_when_configured() { + let mut cfg = cfg(Provider::OpenAi); + cfg.service_tier = Some(crate::config::OpenAiServiceTier::Flex); + let body = openai_body(&cfg, "system", &[], &[], "model", None); + assert_eq!(body["service_tier"], "flex"); + } + + #[test] + fn responses_body_emits_service_tier_when_configured() { + let mut cfg = cfg(Provider::OpenAi); + cfg.service_tier = Some(crate::config::OpenAiServiceTier::Priority); + let body = responses_body(&cfg, "system", &[], &[], "model", None); + assert_eq!(body["service_tier"], "priority"); + } + #[test] fn openai_body_emits_reasoning_effort_medium() { let body = openai_body( diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs index 4d702966b7..bae7437673 100644 --- a/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs +++ b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs @@ -5,6 +5,8 @@ import { BUZZ_AGENT_MAX_CONTEXT_TOKENS, BUZZ_AGENT_MAX_OUTPUT_TOKENS, BUZZ_AGENT_MAX_ROUNDS, + BUZZ_AGENT_SERVICE_TIER, + BUZZ_AGENT_SERVICE_TIER_VALUES, BUZZ_AGENT_THINKING_EFFORT, BUZZ_AGENT_THINKING_EFFORT_VALUES, getProviderEffortConfig, @@ -36,6 +38,16 @@ test("env var key constants match expected BUZZ_AGENT_* names", () => { assert.equal(BUZZ_AGENT_MAX_OUTPUT_TOKENS, "BUZZ_AGENT_MAX_OUTPUT_TOKENS"); assert.equal(BUZZ_AGENT_MAX_CONTEXT_TOKENS, "BUZZ_AGENT_MAX_CONTEXT_TOKENS"); assert.equal(BUZZ_AGENT_MAX_ROUNDS, "BUZZ_AGENT_MAX_ROUNDS"); + assert.equal(BUZZ_AGENT_SERVICE_TIER, "BUZZ_AGENT_SERVICE_TIER"); +}); + +test("BUZZ_AGENT_SERVICE_TIER_VALUES contains the OpenAI service tiers", () => { + assert.deepEqual([...BUZZ_AGENT_SERVICE_TIER_VALUES], [ + "auto", + "default", + "flex", + "priority", + ]); }); // --------------------------------------------------------------------------- @@ -129,6 +141,14 @@ test("clearing max rounds removes the key", () => { assert.equal(Object.hasOwn(result, BUZZ_AGENT_MAX_ROUNDS), false); }); +test("setting and clearing service tier uses the inheritable env-var shape", () => { + const set = applyEnvVarChange({}, BUZZ_AGENT_SERVICE_TIER, "flex"); + assert.equal(set[BUZZ_AGENT_SERVICE_TIER], "flex"); + + const cleared = applyEnvVarChange(set, BUZZ_AGENT_SERVICE_TIER, ""); + assert.equal(Object.hasOwn(cleared, BUZZ_AGENT_SERVICE_TIER), false); +}); + test("changing one field does not disturb other env vars", () => { const initial = { SOME_OTHER_KEY: "value", diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.ts b/desktop/src/features/agents/ui/buzzAgentConfig.ts index a0271fec0f..9b1714b67f 100644 --- a/desktop/src/features/agents/ui/buzzAgentConfig.ts +++ b/desktop/src/features/agents/ui/buzzAgentConfig.ts @@ -17,6 +17,19 @@ 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"; +/** Env var key for the OpenAI request processing tier. */ +export const BUZZ_AGENT_SERVICE_TIER = "BUZZ_AGENT_SERVICE_TIER"; + +/** OpenAI service tiers accepted by buzz-agent. */ +export const BUZZ_AGENT_SERVICE_TIER_VALUES = [ + "auto", + "default", + "flex", + "priority", +] as const; + +export type ServiceTierValue = (typeof BUZZ_AGENT_SERVICE_TIER_VALUES)[number]; + /** * Ordered set of valid thinking-effort values accepted by buzz-agent. * Mirrors `parse_thinking_effort` in `crates/buzz-agent/src/config.rs`. diff --git a/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx b/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx index 938d5edf5b..b993afd419 100644 --- a/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx +++ b/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx @@ -17,6 +17,8 @@ import { BUZZ_AGENT_MAX_CONTEXT_TOKENS, BUZZ_AGENT_MAX_OUTPUT_TOKENS, BUZZ_AGENT_MAX_ROUNDS, + BUZZ_AGENT_SERVICE_TIER, + BUZZ_AGENT_SERVICE_TIER_VALUES, BUZZ_AGENT_THINKING_EFFORT, BUZZ_AGENT_THINKING_EFFORT_VALUES, getProviderEffortConfig, @@ -221,6 +223,7 @@ export function BuzzAgentModelTuningFields({ effortConfig; const currentEffort = envVars[BUZZ_AGENT_THINKING_EFFORT] ?? ""; + const isOpenAi = provider?.trim().toLowerCase() === "openai"; useEffortAutoClear({ currentEffort, effortValid, @@ -258,6 +261,38 @@ export function BuzzAgentModelTuningFields({

+ {isOpenAi && ( +
+ + +

+ Controls OpenAI request processing priority. Leave blank to use + the project default. +

+
+ )} + {/* Max Rounds */}