mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-26 07:26:46 +00:00
fix: streaming robustness — OpenAI parsing, error detection, reasoning content
Improves SSE parsing with raw JSON error detection, HTML response detection (for misconfigured endpoints), thinking/reasoning content from provider-specific delta fields, #[serde(default)] on streaming types for lenient deserialization, compact session boundary guard, and /team slash command. Adds install.sh convenience script.
This commit is contained in:
@@ -505,10 +505,12 @@ impl StreamState {
|
||||
}
|
||||
|
||||
for choice in chunk.choices {
|
||||
// Handle reasoning/thinking from various provider fields
|
||||
if let Some(reasoning) = choice
|
||||
.delta
|
||||
.reasoning_content
|
||||
.filter(|value| !value.is_empty())
|
||||
.or(choice.delta.thinking.and_then(|t| t.content).filter(|value| !value.is_empty()))
|
||||
{
|
||||
if !self.thinking_started {
|
||||
self.thinking_started = true;
|
||||
@@ -736,6 +738,7 @@ impl ToolCallState {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChatCompletionResponse {
|
||||
#[serde(default)]
|
||||
id: String,
|
||||
model: String,
|
||||
choices: Vec<ChatChoice>,
|
||||
@@ -806,6 +809,7 @@ impl OpenAiUsage {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChatCompletionChunk {
|
||||
#[serde(default)]
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
model: Option<String>,
|
||||
@@ -817,6 +821,7 @@ struct ChatCompletionChunk {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ChunkChoice {
|
||||
#[serde(default)]
|
||||
delta: ChunkDelta,
|
||||
#[serde(default)]
|
||||
finish_reason: Option<String>,
|
||||
@@ -826,12 +831,21 @@ struct ChunkChoice {
|
||||
struct ChunkDelta {
|
||||
#[serde(default)]
|
||||
content: Option<String>,
|
||||
/// Some providers (GLM, DeepSeek) emit reasoning in `reasoning_content`
|
||||
#[serde(default)]
|
||||
reasoning_content: Option<String>,
|
||||
#[serde(default)]
|
||||
thinking: Option<ThinkingDelta>,
|
||||
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
|
||||
tool_calls: Vec<DeltaToolCall>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ThinkingDelta {
|
||||
#[serde(default)]
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DeltaToolCall {
|
||||
#[serde(default)]
|
||||
@@ -1455,7 +1469,50 @@ fn parse_sse_frame(
|
||||
data_lines.push(data.trim_start());
|
||||
}
|
||||
}
|
||||
// If no SSE data lines found, check if the entire frame is raw JSON (error or otherwise)
|
||||
if data_lines.is_empty() {
|
||||
// Detect raw JSON error response (not SSE-framed)
|
||||
if let Ok(raw) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||
if let Some(err_obj) = raw.get("error") {
|
||||
let msg = err_obj
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("provider returned an error")
|
||||
.to_string();
|
||||
let code = err_obj
|
||||
.get("code")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|c| c as u16);
|
||||
let status = reqwest::StatusCode::from_u16(code.unwrap_or(500))
|
||||
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
|
||||
return Err(ApiError::Api {
|
||||
status,
|
||||
error_type: err_obj
|
||||
.get("type")
|
||||
.and_then(|t| t.as_str())
|
||||
.map(str::to_owned),
|
||||
message: Some(msg),
|
||||
request_id: None,
|
||||
body: trimmed.chars().take(500).collect(),
|
||||
retryable: false,
|
||||
suggested_action: suggested_action_for_status(status),
|
||||
retry_after: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Detect HTML responses
|
||||
if trimmed.starts_with('<') || trimmed.starts_with("<!") {
|
||||
return Err(ApiError::Api {
|
||||
status: reqwest::StatusCode::BAD_REQUEST,
|
||||
error_type: Some("invalid_response".to_string()),
|
||||
message: Some("provider returned HTML instead of JSON (check endpoint URL)".to_string()),
|
||||
request_id: None,
|
||||
body: trimmed.chars().take(200).collect(),
|
||||
retryable: false,
|
||||
suggested_action: Some("verify the API endpoint URL is correct".to_string()),
|
||||
retry_after: None,
|
||||
});
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
let payload = data_lines.join("\n");
|
||||
@@ -1492,6 +1549,20 @@ fn parse_sse_frame(
|
||||
});
|
||||
}
|
||||
}
|
||||
// Detect HTML or other non-JSON responses early for better error messages
|
||||
let trimmed_payload = payload.trim();
|
||||
if trimmed_payload.starts_with('<') || trimmed_payload.starts_with("<!") {
|
||||
return Err(ApiError::Api {
|
||||
status: reqwest::StatusCode::BAD_REQUEST,
|
||||
error_type: Some("invalid_response".to_string()),
|
||||
message: Some("provider returned HTML instead of JSON (check endpoint URL)".to_string()),
|
||||
request_id: None,
|
||||
body: payload.chars().take(200).collect(),
|
||||
retryable: false,
|
||||
suggested_action: Some("verify the API endpoint URL is correct".to_string()),
|
||||
retry_after: None,
|
||||
});
|
||||
}
|
||||
serde_json::from_str::<ChatCompletionChunk>(&payload)
|
||||
.map(Some)
|
||||
.map_err(|error| ApiError::json_deserialize(provider, model, &payload, error))
|
||||
|
||||
@@ -1472,6 +1472,7 @@ pub fn validate_slash_command_input(
|
||||
}
|
||||
"plan" => SlashCommand::Plan { mode: remainder },
|
||||
"review" => SlashCommand::Review { scope: remainder },
|
||||
"team" => SlashCommand::Team { action: remainder },
|
||||
"tasks" => SlashCommand::Tasks { args: remainder },
|
||||
"theme" => SlashCommand::Theme { name: remainder },
|
||||
"voice" => SlashCommand::Voice { mode: remainder },
|
||||
|
||||
@@ -128,7 +128,7 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
|
||||
// is NOT an assistant message that contains a ToolUse block (i.e. the
|
||||
// pair is actually broken at the boundary).
|
||||
loop {
|
||||
if k == 0 || k <= compacted_prefix_len {
|
||||
if k == 0 || k <= compacted_prefix_len || k >= session.messages.len() {
|
||||
break;
|
||||
}
|
||||
let first_preserved = &session.messages[k];
|
||||
|
||||
11
rust/scripts/install.sh
Executable file
11
rust/scripts/install.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Build the release binary
|
||||
cargo build --release
|
||||
|
||||
# Link to ~/.local/bin
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
ln -sf "$(pwd)/target/release/claw" "$HOME/.local/bin/claw"
|
||||
|
||||
echo "✓ Claw installed to ~/.local/bin/claw"
|
||||
Reference in New Issue
Block a user