Преглед изворни кода

fix: log empty successful responses as request errors

pull/1/head
orangels пре 1 месец
родитељ
комит
b5d83f948b
3 измењених фајлова са 274 додато и 17 уклоњено
  1. +139
    -10
      src/protocols/anthropic.rs
  2. +67
    -3
      src/protocols/google.rs
  3. +68
    -4
      src/protocols/openai.rs

+ 139
- 10
src/protocols/anthropic.rs Прегледај датотеку

@@ -76,16 +76,38 @@ pub async fn send(client: &Client, request: &ModelRequest) -> Result<ModelRespon
); );
} }


let parsed: AnthropicResponse =
serde_json::from_str(&body).context("failed to parse Anthropic response JSON")?;
let text = parsed
.content
.into_iter()
.find_map(|block| match block {
AnthropicContentBlock::Text { text } if !text.is_empty() => Some(text),
_ => None,
})
.context("Anthropic response missing first text content block")?;
let parsed: AnthropicResponse = match serde_json::from_str(&body) {
Ok(parsed) => parsed,
Err(error) => {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
format!("failed to parse Anthropic response JSON: {error}"),
)
.await?;
return Err(error).context("failed to parse Anthropic response JSON");
}
};
let Some(text) = parsed.content.into_iter().find_map(|block| match block {
AnthropicContentBlock::Text { text } if !text.is_empty() => Some(text),
_ => None,
}) else {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
"Anthropic response missing first text content block".to_string(),
)
.await?;
bail!("Anthropic response missing first text content block");
};


Ok(ModelResponse { Ok(ModelResponse {
text, text,
@@ -224,6 +246,16 @@ pub async fn send_stream(client: &Client, request: &ModelRequest) -> Result<Mode
} }


if text.is_empty() { if text.is_empty() {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&raw_stream,
"Anthropic stream completed without producing any content".to_string(),
)
.await?;
bail!("Anthropic stream completed without producing any content"); bail!("Anthropic stream completed without producing any content");
} }


@@ -322,6 +354,34 @@ async fn write_error_debug(
Ok(()) Ok(())
} }


async fn write_response_request_error_debug(
request: &ModelRequest,
url: &str,
request_body: Value,
status: u16,
response_headers: &reqwest::header::HeaderMap,
response_body: &str,
error: String,
) -> Result<()> {
if let Some(raw_debug) = &request.raw_debug {
raw_debug
.write_http_error(
"anthropic-request-error",
debug_request(request, url, request_body),
HttpDebugResponse {
status: Some(status),
headers: response_headers_for_debug(response_headers),
body: response_body.to_string(),
error_kind: Some("request_error".to_string()),
error: Some(error),
},
)
.await
.context("failed to write Anthropic raw debug request error response")?;
}
Ok(())
}

fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest { fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest {
HttpDebugRequest { HttpDebugRequest {
method: "POST".to_string(), method: "POST".to_string(),
@@ -762,6 +822,75 @@ mod tests {
assert!(raw.contains("\"text\":\"hi \"")); assert!(raw.contains("\"text\":\"hi \""));
} }


#[tokio::test]
async fn empty_stream_request_error_debug_records_request_and_raw_stream() {
let server = MockServer::start().await;
let temp_dir = tempfile::tempdir().expect("create temp dir");
Mock::given(method("POST"))
.and(path("/v1/messages"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(
"event: message_start\n\
data: {\"type\":\"message_start\"}\n\n\
event: message_stop\n\
data: {\"type\":\"message_stop\"}\n\n",
),
)
.mount(&server)
.await;

let request = ModelRequest {
base_url: server.uri(),
api_token: "anthropic-secret-token".to_string(),
model: "claude-test".to_string(),
prompt: "prompt before empty stream".to_string(),
temperature: 0.0,
max_tokens: 1024,
stream: true,
raw_debug: Some(
RawDebugConfig::new(
temp_dir.path().to_path_buf(),
"anthropic-claude-test".to_string(),
)
.with_success_raw(false),
),
thinking: None,
};

let error = super::send_stream(&Client::new(), &request)
.await
.expect_err("empty stream should fail");
assert!(
error
.to_string()
.contains("completed without producing any content")
);

let debug_files = std::fs::read_dir(temp_dir.path())
.expect("read debug dir")
.collect::<Result<Vec<_>, _>>()
.expect("debug entries");
assert_eq!(debug_files.len(), 1);
let raw = std::fs::read_to_string(debug_files[0].path()).expect("read raw debug file");
let debug: serde_json::Value = serde_json::from_str(&raw).expect("debug json");

assert_eq!(
debug["request"]["body"]["messages"][0]["content"],
"prompt before empty stream"
);
assert_eq!(debug["response"]["status"], 200);
assert_eq!(debug["response"]["error_kind"], "request_error");
assert!(
debug["response"]["body"]
.as_str()
.expect("response body")
.contains("event: message_stop")
);
assert!(!raw.contains("anthropic-secret-token"));
}

fn anthropic_request(base_url: String) -> ModelRequest { fn anthropic_request(base_url: String) -> ModelRequest {
ModelRequest { ModelRequest {
base_url, base_url,


+ 67
- 3
src/protocols/google.rs Прегледај датотеку

@@ -75,9 +75,35 @@ pub async fn send(client: &Client, request: &ModelRequest) -> Result<ModelRespon
); );
} }


let parsed: GoogleResponse =
serde_json::from_str(&body).context("failed to parse Google response JSON")?;
let text = response_text(parsed).context("Google response missing non-thought text part")?;
let parsed: GoogleResponse = match serde_json::from_str(&body) {
Ok(parsed) => parsed,
Err(error) => {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
format!("failed to parse Google response JSON: {error}"),
)
.await?;
return Err(error).context("failed to parse Google response JSON");
}
};
let Some(text) = response_text(parsed) else {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
"Google response missing non-thought text part".to_string(),
)
.await?;
bail!("Google response missing non-thought text part");
};


Ok(ModelResponse { Ok(ModelResponse {
text, text,
@@ -200,6 +226,16 @@ pub async fn send_stream(client: &Client, request: &ModelRequest) -> Result<Mode
} }


if text.is_empty() { if text.is_empty() {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&raw_stream,
"Google stream completed without producing any content".to_string(),
)
.await?;
bail!("Google stream completed without producing any content"); bail!("Google stream completed without producing any content");
} }


@@ -296,6 +332,34 @@ async fn write_error_debug(
Ok(()) Ok(())
} }


async fn write_response_request_error_debug(
request: &ModelRequest,
url: &str,
request_body: Value,
status: u16,
response_headers: &reqwest::header::HeaderMap,
response_body: &str,
error: String,
) -> Result<()> {
if let Some(raw_debug) = &request.raw_debug {
raw_debug
.write_http_error(
"google-request-error",
debug_request(request, url, request_body),
HttpDebugResponse {
status: Some(status),
headers: response_headers_for_debug(response_headers),
body: response_body.to_string(),
error_kind: Some("request_error".to_string()),
error: Some(error),
},
)
.await
.context("failed to write Google raw debug request error response")?;
}
Ok(())
}

fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest { fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest {
HttpDebugRequest { HttpDebugRequest {
method: "POST".to_string(), method: "POST".to_string(),


+ 68
- 4
src/protocols/openai.rs Прегледај датотеку

@@ -75,15 +75,41 @@ pub async fn send(client: &Client, request: &ModelRequest) -> Result<ModelRespon
); );
} }


let parsed: OpenAiResponse =
serde_json::from_str(&body).context("failed to parse OpenAI response JSON")?;
let text = parsed
let parsed: OpenAiResponse = match serde_json::from_str(&body) {
Ok(parsed) => parsed,
Err(error) => {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
format!("failed to parse OpenAI response JSON: {error}"),
)
.await?;
return Err(error).context("failed to parse OpenAI response JSON");
}
};
let Some(text) = parsed
.choices .choices
.into_iter() .into_iter()
.next() .next()
.and_then(|choice| choice.message.content) .and_then(|choice| choice.message.content)
.filter(|content| !content.is_empty()) .filter(|content| !content.is_empty())
.context("OpenAI response missing choices[0].message.content")?;
else {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&body,
"OpenAI response missing choices[0].message.content".to_string(),
)
.await?;
bail!("OpenAI response missing choices[0].message.content");
};


Ok(ModelResponse { Ok(ModelResponse {
text, text,
@@ -204,6 +230,16 @@ pub async fn send_stream(client: &Client, request: &ModelRequest) -> Result<Mode
} }


if text.is_empty() { if text.is_empty() {
write_response_request_error_debug(
request,
&debug_url,
request_body,
status_code,
&response_headers,
&raw_stream,
"OpenAI stream completed without producing any content".to_string(),
)
.await?;
bail!("OpenAI stream completed without producing any content"); bail!("OpenAI stream completed without producing any content");
} }


@@ -303,6 +339,34 @@ async fn write_error_debug(
Ok(()) Ok(())
} }


async fn write_response_request_error_debug(
request: &ModelRequest,
url: &str,
request_body: Value,
status: u16,
response_headers: &reqwest::header::HeaderMap,
response_body: &str,
error: String,
) -> Result<()> {
if let Some(raw_debug) = &request.raw_debug {
raw_debug
.write_http_error(
"openai-request-error",
debug_request(request, url, request_body),
HttpDebugResponse {
status: Some(status),
headers: response_headers_for_debug(response_headers),
body: response_body.to_string(),
error_kind: Some("request_error".to_string()),
error: Some(error),
},
)
.await
.context("failed to write OpenAI raw debug request error response")?;
}
Ok(())
}

fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest { fn debug_request(request: &ModelRequest, url: &str, body: Value) -> HttpDebugRequest {
HttpDebugRequest { HttpDebugRequest {
method: "POST".to_string(), method: "POST".to_string(),


Loading…
Откажи
Сачувај