From ac729162e5fc9185f6381a1ea861e32353d02102 Mon Sep 17 00:00:00 2001 From: Gabriel Fonseca Date: Thu, 12 Nov 2026 14:44:11 +0000 Subject: [PATCH 1/2] fix(providers): tolerate null tool_calls in OpenAI-compatible responses Some OpenAI-compatible hosts (e.g. SGLang) send "tool_calls": null instead of omitting the field. Serde's #[serde(default)] only fills missing keys, so deserialization failed with 'invalid type: null, expected a sequence' on every request. Treat an explicit null as an empty tool-call list in both the non-streaming (ChatMessage) and streaming (ChunkDelta) paths. Backward compatible: missing keys still default to empty. --- crates/code-providers/src/providers/openai.rs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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, } From d69df1316f7d0ac7f42f7af5de3368f1048c2ab0 Mon Sep 17 00:00:00 2001 From: Gabriel Fonseca Date: Thu, 12 Nov 2026 14:53:02 +0000 Subject: [PATCH 2/2] docs(cli): rename frontal-code-compat-harness to code-compat-harness --- cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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