diff --git a/cli/README.md b/cli/README.md index 96636b8..4eace0b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -35,7 +35,7 @@ The Frontal Code CLI is the primary user interface for interacting with the Fron - `code-api` for AI provider integration and HTTP services - `code-commands` for slash command system and registry -- `frontal-code-compat-harness` for testing and compatibility validation +- `code-compat-harness` for testing and compatibility validation - `code-runtime` for core functionality and session management - `code-plugins` for extensibility and plugin management - `code-tools` for tool integration and execution diff --git a/crates/code-providers/src/providers/openai.rs b/crates/code-providers/src/providers/openai.rs index 8722c2a..6a38dbf 100644 --- a/crates/code-providers/src/providers/openai.rs +++ b/crates/code-providers/src/providers/openai.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, VecDeque}; use std::time::Duration; +use serde::de::DeserializeOwned; use serde::Deserialize; use serde_json::{json, Value}; @@ -627,10 +628,27 @@ struct ChatMessage { role: String, #[serde(default)] content: Option, - #[serde(default)] + // Some OpenAI-compatible hosts (e.g. `SGLang`) send `"tool_calls": null` + // instead of omitting the field. `#[serde(default)]` only fills missing + // keys, so tolerate an explicit null as "no tool calls". + #[serde(default, deserialize_with = "deser_null_as_empty_seq")] tool_calls: Vec, } +/// Deserialize a sequence, treating an explicit JSON `null` as empty. +/// +/// Serde's `#[serde(default)]` only substitutes a value for *missing* keys, so +/// a host that emits `"tool_calls": null` (e.g. `SGLang`) would otherwise fail +/// with "invalid type: null, expected a sequence". +fn deser_null_as_empty_seq<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: DeserializeOwned, +{ + let value = Option::>::deserialize(deserializer)?; + Ok(value.unwrap_or_default()) +} + #[derive(Debug, Deserialize)] struct ResponseToolCall { id: String, @@ -673,7 +691,7 @@ struct ChunkChoice { struct ChunkDelta { #[serde(default)] content: Option, - #[serde(default)] + #[serde(default, deserialize_with = "deser_null_as_empty_seq")] tool_calls: Vec, }