mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
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:
1 parent
174a5f3cc3
commit
72ed264f09
3 files changed
+58
-3
No files matched your search
+11
-1
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user