fix(claude): pair diagnostics with cache beta

Derive cache-diagnosis from the final Messages body so cloaked OAuth requests reproduce Claude Code's exact diagnostics trailer instead of receiving an Anthropic 400.
This commit is contained in:
sususu
2026-08-03 15:43:50 +08:00
parent 43f8146413
commit 56e533fb96
3 changed files with 41 additions and 7 deletions

View File

@@ -48,10 +48,11 @@ func TestApplyClaudeHeaders_ConfirmedClientKeepsOAuthCredentialBetas(t *testing.
if parts[len(parts)-1] != claudeExtendedCacheTTLBeta {
t.Fatalf("Anthropic-Beta = %q, want OAuth cache trailer %s", got, claudeExtendedCacheTTLBeta)
}
for _, stale := range []string{"advisor-tool-2026-03-01", "cache-diagnosis-2026-04-07"} {
if strings.Contains(got, stale) {
t.Fatalf("Anthropic-Beta = %q, contains stale OAuth beta %s", got, stale)
}
if strings.Contains(got, "advisor-tool-2026-03-01") {
t.Fatalf("Anthropic-Beta = %q, contains stale OAuth tool beta", got)
}
if strings.Contains(got, claudeCacheDiagnosisBeta) {
t.Fatalf("Anthropic-Beta = %q, contains %s without a diagnostics body", got, claudeCacheDiagnosisBeta)
}
// The caller's own betas survive the restoration.
for _, want := range []string{"interleaved-thinking-2025-05-14", claudeEffortBeta} {
@@ -221,8 +222,24 @@ func TestApplyClaudeHeaders_FastModePrecedesOAuthTrailer(t *testing.T) {
if parts[len(parts)-2] != claudeFastModeBeta {
t.Fatalf("Anthropic-Beta = %q, want %s before the OAuth cache trailer", got, claudeFastModeBeta)
}
if strings.Contains(got, "cache-diagnosis-2026-04-07") {
t.Fatalf("Anthropic-Beta = %q, contains stale cache diagnosis beta", got)
if strings.Contains(got, claudeCacheDiagnosisBeta) {
t.Fatalf("Anthropic-Beta = %q, contains %s without a diagnostics body", got, claudeCacheDiagnosisBeta)
}
}
func TestApplyClaudeHeaders_DiagnosticsBetaFollowsBodyInNativeOrder(t *testing.T) {
for _, stream := range []bool{false, true} {
req := newClaudeHeaderTestRequest(t, nil)
body := []byte(`{"model":"claude-opus-5","diagnostics":{"previous_message_id":null}}`)
if err := applyClaudeHeaders(req, claudeOAuthAuthForBetaPolicy(), claudeRaceProbeOAuthKey, stream, nil,
body, nil, nil, false); err != nil {
t.Fatalf("applyClaudeHeaders(stream=%v) error = %v", stream, err)
}
got := req.Header.Get("Anthropic-Beta")
wantTrailer := claudeExtendedCacheTTLBeta + "," + claudeCacheDiagnosisBeta
if !strings.HasSuffix(got, wantTrailer) {
t.Fatalf("stream=%v: Anthropic-Beta = %q, want native diagnostics trailer %q", stream, got, wantTrailer)
}
}
}

View File

@@ -41,6 +41,7 @@ func TestInjectClaudeDiagnosticsMatchesNativeFieldOrderAndContinuity(t *testing.
func TestClaudeExecutorDiagnosticsAdvancesAfterSuccessfulResponse(t *testing.T) {
var previousValues []gjson.Result
var betaValues []string
call := 0
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
body, errRead := io.ReadAll(req.Body)
@@ -48,6 +49,11 @@ func TestClaudeExecutorDiagnosticsAdvancesAfterSuccessfulResponse(t *testing.T)
t.Fatal(errRead)
}
previousValues = append(previousValues, gjson.GetBytes(body, "diagnostics.previous_message_id"))
betas := req.Header.Get("Anthropic-Beta")
if betas == "" {
betas = strings.Join(req.Header["anthropic-beta"], ",")
}
betaValues = append(betaValues, betas)
call++
response := `{"id":"msg_diagnostics_` + string(rune('0'+call)) + `","type":"message","model":"claude-opus-5","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`
return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(response)), Request: req}, nil
@@ -83,6 +89,12 @@ func TestClaudeExecutorDiagnosticsAdvancesAfterSuccessfulResponse(t *testing.T)
if got := previousValues[1].String(); got != "msg_diagnostics_1" {
t.Fatalf("second diagnostics previous_message_id = %q, want first upstream response ID", got)
}
wantTrailer := claudeExtendedCacheTTLBeta + "," + claudeCacheDiagnosisBeta
for turn, betas := range betaValues {
if !strings.HasSuffix(betas, wantTrailer) {
t.Fatalf("turn %d Anthropic-Beta = %q, want native diagnostics trailer %q", turn+1, betas, wantTrailer)
}
}
}
func TestClaudeMessageIDFromSSECommitsOnlyCompletedMessage(t *testing.T) {

View File

@@ -42,6 +42,7 @@ const (
claudeFallbackCreditBeta = "fallback-credit-2026-06-01"
claudeStructuredOutputsBeta = "structured-outputs-2025-12-15"
claudeExtendedCacheTTLBeta = "extended-cache-ttl-2025-04-11"
claudeCacheDiagnosisBeta = "cache-diagnosis-2026-04-07"
)
// claudeCodeCLIConstantBetas are the betas Claude Code 2.1.220 sends on every
@@ -91,11 +92,12 @@ var claudeCodeTrailingBetas = []string{
// 13 fallback-credit-2026-06-01
// 14 fast-mode-2026-02-01 speed:fast requests only
// 15 extended-cache-ttl-2025-04-11 OAuth credentials only
// 16 cache-diagnosis-2026-04-07 requests with diagnostics only
//
// An empty body keeps the optimistic role=system default, matching the cloaking
// policy for unknown and future model IDs.
func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool) string {
betas := make([]string, 0, len(claudeCodeCLIConstantBetas)+len(claudeCodeTrailingBetas)+6)
betas := make([]string, 0, len(claudeCodeCLIConstantBetas)+len(claudeCodeTrailingBetas)+7)
betas = append(betas, claudeCodeBeta)
if oauthToken {
betas = append(betas, claudeOAuthBeta)
@@ -125,6 +127,9 @@ func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool)
if oauthToken {
betas = append(betas, claudeExtendedCacheTTLBeta)
}
if diagnostics := gjson.GetBytes(body, "diagnostics"); diagnostics.IsObject() {
betas = append(betas, claudeCacheDiagnosisBeta)
}
return strings.Join(betas, ",")
}