fix(client): explain missing Google signatures after gateway rejection

Append bounded recovery guidance only to HTTP 400 responses reporting a missing thought_signature. Both Chat request paths use this sanitizer; generic gateway preflight remains unchanged. Add release notes for this repair, Markdown clipboard behavior and the embedded CU JPEG port.

Validation: hermetic llm_client and Google signature nextest selection: 41 tests run, 41 passed, 12357 skipped. TUI library cargo check passed. Version check and documentation check (23 topics) passed; git diff --check passed. Root npm gate passed on the gate-repair slice: wrapper 66, SDK 9, web 446 tests; final-head full sweep follows.
This commit is contained in:
CodeWhale Bot committed 2026-09-11 01:56:14 -07:00
1 parent 174a5f3cc3
commit 72ed264f09
3 files changed
+58 -3

No files matched your search

+11 -1
View File
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.13] - 2026-09-10
## [0.9.13] - 2026-09-11
Codewhale v0.9.13 source candidate addresses integrity issues in 0.9.12:
multiline paste is one paste again, truncated tool arguments can no longer execute, strict
@@ -21,6 +21,16 @@ reconnect.
### Fixed
- Direct composer paste (`Ctrl-V`) preserves rich clipboard headings, lists,
links, tables and code as Markdown, with plain-text fallback. Whole-answer
copy preserves authored Markdown without terminal wrapping; settings fields
still paste literal text.
- The embedded Computer Use bundle includes the marketplace's JPEG capture
and bounded-payload fixes, so new built-in installations receive them too.
- A provider's missing-`thought_signature` HTTP 400 now explains how to recover:
use the built-in Google provider and start a new session, or verify the
gateway's signature handling. Working gateways are not blocked by a broader
preflight check (#6048).
- The sandbox-elevation prompt shows every option, `Abort` included. The card
was a fixed 22 rows centred on the frame; inside its border and padding that
left at most 18 usable rows against 20 to 23 rows of content, with no scroll
+19 -2
View File
@@ -653,7 +653,7 @@ pub(crate) fn sanitize_http_error_body(
if let Some(code) = explicit_quota_code(body) {
return format!("{message} (provider error code: {code})");
}
return message;
return with_thought_signature_recovery(status, message);
}
if is_probably_html(body) {
@@ -696,7 +696,24 @@ pub(crate) fn sanitize_http_error_body(
return format!("{provider} API returned an HTML error page (HTTP {status}): {text}");
}
truncate_for_error(&collapse_whitespace(body), 2_000)
with_thought_signature_recovery(
status,
truncate_for_error(&collapse_whitespace(body), 2_000),
)
}
/// A gateway may manage signatures itself, so only explain an actual rejection
/// instead of preemptively blocking every Gemini-compatible route.
fn with_thought_signature_recovery(status: u16, mut message: String) -> String {
let lower = message.to_ascii_lowercase();
if status == 400 && lower.contains("missing") && lower.contains("thought_signature") {
const HINT: &str = " Use the built-in `google` provider for Gemini and start a new session; \
earlier tool calls cannot recover missing thought signatures. If you use a \
gateway, confirm that it preserves or manages Google thought signatures.";
message = truncate_for_error(&message, 2_000 - HINT.len());
message.push_str(HINT);
}
message
}
fn looks_like_authentication_failure(body: &str) -> bool {
+28
View File
@@ -150,6 +150,34 @@ fn generic_429_stays_rate_limited_and_retryable() {
assert!(error.is_retryable());
}
#[test]
fn missing_google_signature_400_explains_recovery_without_widening_gateway_preflight() {
let message = "Function call is missing a thought_signature in functionCall parts";
for body in [
message.to_string(),
serde_json::json!({"error": {"message": message}}).to_string(),
format!("{message} {}", "详情".repeat(2_000)),
] {
let safe = sanitize_http_error_body(Some("OpenAI-compatible"), 400, &body);
assert!(safe.starts_with(message));
assert!(safe.contains("built-in `google` provider"));
assert!(safe.contains("start a new session"));
assert!(safe.contains("gateway"));
assert!(
safe.chars().count() <= 2_003,
"error bound survives the hint"
);
}
for (status, body) in [
(429, message),
(200, message),
(400, "Invalid thought_signature"),
(400, "Missing required parameter: model"),
] {
assert_eq!(sanitize_http_error_body(None, status, body), body);
}
}
#[tokio::test]
async fn retry_loop_stops_after_one_typed_quota_failure() {
let mut calls = 0;