copilot_chat: Serialize ToolChoice::Any as "required" instead of "any" (#52015)

Valery Borovsky created

**Observed behavior:** Inline assistant failed for GPT-4.1,
Gemini, and other non-Anthropic models. Claude worked correctly because
Anthropic's API accepts `"any"` as a valid value.

## Fix
Renamed `ToolChoice::Any` → `ToolChoice::Required` in both
`copilot_chat.rs` and `responses.rs`, matching the convention used by
other OpenAI-compatible providers (`open_ai`, `lmstudio`,
`open_router`).

`copilot_chat::ToolChoice` is a wire type only for the
`/chat/completions`
path — Anthropic models go through `into_anthropic()` and never touch
it,
so no per-model logic is needed.

Also fixes the same serialization bug in `responses::ToolChoice`, which
was not covered by the original approach, and adds regression tests for
both.

## Affected models
- `gpt-4.1` via copilot_chat provider
- `gemini-*` via copilot_chat provider
- Likely affects all OpenAI-compatible models routed through
copilot_chat

## Screenshots
**Bug (only Claude works, Gemini and GPT-4.1 fail):**
<img width="598" height="209" alt="image"
src="https://github.com/user-attachments/assets/bbd418d9-7de3-4191-9ca9-fd1961534e23"
/>

**Fix:**
<img width="532" height="154" alt="image"
src="https://github.com/user-attachments/assets/86bb0f8e-67e6-4417-9b78-b1b7ad328e9e"
/>

**Result:** After the fix, all models work correctly via inline
assistant.

## Release Notes
- Fix inline assistant 400 errors for GPT-4.1, Gemini, and other
  non-Anthropic models via the copilot_chat provider (`tool_choice` was
  sending `"any"` instead of `"required"`)

Change summary

crates/copilot_chat/src/copilot_chat.rs             | 20 +++++++++++
crates/copilot_chat/src/responses.rs                | 25 ++++++++++++++
crates/language_models/src/provider/copilot_chat.rs |  4 +-
3 files changed, 45 insertions(+), 4 deletions(-)

Detailed changes

crates/copilot_chat/src/copilot_chat.rs 🔗

@@ -370,7 +370,7 @@ pub enum Tool {
 #[serde(rename_all = "lowercase")]
 pub enum ToolChoice {
     Auto,
-    Any,
+    Required,
     None,
 }
 
@@ -1736,4 +1736,22 @@ mod tests {
         // Only /v1/messages endpoint -> supports_response = false (doesn't have /responses)
         assert!(!model_with_messages.supports_response());
     }
+
+    #[test]
+    fn test_tool_choice_required_serializes_as_required() {
+        // Regression test: ToolChoice::Required must serialize as "required" (not "any")
+        // for OpenAI-compatible APIs. Reverting the rename would break this.
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::Required).unwrap(),
+            "\"required\""
+        );
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::Auto).unwrap(),
+            "\"auto\""
+        );
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::None).unwrap(),
+            "\"none\""
+        );
+    }
 }

crates/copilot_chat/src/responses.rs 🔗

@@ -52,7 +52,7 @@ pub enum ToolDefinition {
 #[serde(rename_all = "lowercase")]
 pub enum ToolChoice {
     Auto,
-    Any,
+    Required,
     None,
     #[serde(untagged)]
     Other(ToolDefinition),
@@ -408,3 +408,26 @@ pub async fn stream_response(
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_tool_choice_required_serializes_as_required() {
+        // Regression test: ToolChoice::Required must serialize as "required" (not "any")
+        // for OpenAI Responses API. Reverting the rename would break this.
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::Required).unwrap(),
+            "\"required\""
+        );
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::Auto).unwrap(),
+            "\"auto\""
+        );
+        assert_eq!(
+            serde_json::to_string(&ToolChoice::None).unwrap(),
+            "\"none\""
+        );
+    }
+}

crates/language_models/src/provider/copilot_chat.rs 🔗

@@ -1046,7 +1046,7 @@ fn into_copilot_chat(
         tools,
         tool_choice: tool_choice.map(|choice| match choice {
             LanguageModelToolChoice::Auto => ToolChoice::Auto,
-            LanguageModelToolChoice::Any => ToolChoice::Any,
+            LanguageModelToolChoice::Any => ToolChoice::Required,
             LanguageModelToolChoice::None => ToolChoice::None,
         }),
         thinking_budget: None,
@@ -1255,7 +1255,7 @@ fn into_copilot_responses(
 
     let mapped_tool_choice = tool_choice.map(|choice| match choice {
         LanguageModelToolChoice::Auto => responses::ToolChoice::Auto,
-        LanguageModelToolChoice::Any => responses::ToolChoice::Any,
+        LanguageModelToolChoice::Any => responses::ToolChoice::Required,
         LanguageModelToolChoice::None => responses::ToolChoice::None,
     });