diff --git a/crates/protocols/src/responses.rs b/crates/protocols/src/responses.rs index ca6440b07..21d3a90be 100644 --- a/crates/protocols/src/responses.rs +++ b/crates/protocols/src/responses.rs @@ -452,6 +452,30 @@ pub enum ResponseTool { LocalShell, } +impl ResponseTool { + /// Wire `type` tag for this variant — matches the variant's + /// `#[serde(rename = ...)]` attribute. Stable across serde + /// roundtrips and safe to use as a discriminator string. + pub fn as_str(&self) -> &'static str { + match self { + ResponseTool::Function(_) => "function", + ResponseTool::WebSearchPreview(_) => "web_search_preview", + ResponseTool::WebSearch(_) => "web_search", + ResponseTool::CodeInterpreter(_) => "code_interpreter", + ResponseTool::Mcp(_) => "mcp", + ResponseTool::FileSearch(_) => "file_search", + ResponseTool::ImageGeneration(_) => "image_generation", + ResponseTool::Computer => "computer", + ResponseTool::ComputerUsePreview(_) => "computer_use_preview", + ResponseTool::Custom(_) => "custom", + ResponseTool::Namespace(_) => "namespace", + ResponseTool::Shell(_) => "shell", + ResponseTool::ApplyPatch => "apply_patch", + ResponseTool::LocalShell => "local_shell", + } + } +} + /// Payload carried by [`ResponseTool::Namespace`]. /// /// Using a dedicated struct (rather than inline struct-variant fields) lets @@ -3883,3 +3907,24 @@ impl ResponseReasoningContent { Self::ReasoningText { text } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Lock `as_str()` to the canonical serde tag for the unit variants + /// — drift between the two would produce inconsistent wire labels + /// across dispatch paths and serializers. + #[test] + fn response_tool_as_str_matches_serde_tag_for_unit_variants() { + for tool in [ + ResponseTool::Computer, + ResponseTool::ApplyPatch, + ResponseTool::LocalShell, + ] { + let serialized = serde_json::to_value(&tool).unwrap(); + let serde_tag = serialized.get("type").and_then(|v| v.as_str()).unwrap(); + assert_eq!(tool.as_str(), serde_tag); + } + } +} diff --git a/model_gateway/src/routers/grpc/harmony/builder.rs b/model_gateway/src/routers/grpc/harmony/builder.rs index 3efaacd30..f10efeba3 100644 --- a/model_gateway/src/routers/grpc/harmony/builder.rs +++ b/model_gateway/src/routers/grpc/harmony/builder.rs @@ -543,28 +543,7 @@ impl HarmonyBuilder { let tool_types: Vec<&str> = request .tools .as_ref() - .map(|tools| { - tools - .iter() - .map(|tool| match tool { - ResponseTool::Function(_) => "function", - ResponseTool::WebSearchPreview(_) => "web_search_preview", - ResponseTool::WebSearch(_) => "web_search", - ResponseTool::CodeInterpreter(_) => "code_interpreter", - ResponseTool::Mcp(_) => "mcp", - ResponseTool::FileSearch(_) => "file_search", - ResponseTool::ImageGeneration(_) => "image_generation", - ResponseTool::Computer => "computer", - ResponseTool::ComputerUsePreview(_) => "computer_use_preview", - ResponseTool::Custom(_) => "custom", - ResponseTool::Namespace(_) => "namespace", - ResponseTool::Shell(_) => "shell", - ResponseTool::ApplyPatch => "apply_patch", - // T5 schema-only: forced-cascade arm, no behavior. - ResponseTool::LocalShell => "local_shell", - }) - .collect() - }) + .map(|tools| tools.iter().map(ResponseTool::as_str).collect()) .unwrap_or_default(); let with_custom_tools = has_custom_tools(&tool_types);