Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,9 @@ func responsesSystemUnsupportedBlock(part gjson.Result) []byte {
// thought. Anthropic requires a signature on every thinking block and rejects an
// absent or empty one, so an item whose encrypted_content is missing or belongs
// to another provider is dropped rather than replayed as an unsigned block.
// Compatibility mode explicitly keeps the original opaque value as the
// signature for upstreams that use a provider-specific signature format.
// Compatibility mode may emit an unsigned thinking block for configured
// compatibility endpoints that accept one, but it never forwards an opaque or
// foreign encrypted_content as a valid Claude signature.
// Anthropic does not verify the text against the signature, which is what makes
// the summarized text safe to restore alongside it.
func convertResponsesReasoningToClaudeThinking(item gjson.Result, preserveEmptyThinkingBlocks ...bool) []byte {
Expand All @@ -589,7 +590,9 @@ func convertResponsesReasoningToClaudeThinking(item gjson.Result, preserveEmptyT
if !preserveEmpty {
return nil
}
signature = encrypted
// Compat mode is allowed to emit an unsigned thinking block, but it must
// not pass a foreign or opaque encrypted_content through as a signature.
signature = ""
}

thinkingText := responsesReasoningText(item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestConvertOpenAIResponsesRequestToClaudeWithCompatPreservesEmptyReasoning(
opaquePayload := []byte(`{"input":[{"type":"reasoning","summary":[{"type":"summary_text","text":"reason"}],"encrypted_content":"opaque-deepseek-id"}]}`)
opaqueCompat := ConvertOpenAIResponsesRequestToClaudeWithCompat("deepseek-v4", opaquePayload, false)
opaquePart := gjson.GetBytes(opaqueCompat, "messages.0.content.0")
if opaquePart.Get("type").String() != "thinking" || opaquePart.Get("thinking").String() != "reason" || opaquePart.Get("signature").String() != "opaque-deepseek-id" {
t.Fatalf("compat translation dropped invalid-signature thinking block: %s", opaqueCompat)
if opaquePart.Get("type").String() != "thinking" || opaquePart.Get("thinking").String() != "reason" || opaquePart.Get("signature").String() != "" {
t.Fatalf("compat translation must sanitize invalid-signature thinking block but preserve text: %s", opaqueCompat)
}
}
12 changes: 11 additions & 1 deletion internal/translator/gemini/claude/gemini_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
sigcompat "github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
Expand Down Expand Up @@ -117,9 +118,18 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool,
if !preserveEmptyThinkingBlocks {
return true
}
rawSignature := contentResult.Get("signature").String()
thoughtSignature := rawSignature
if rawSignature != "" {
if sig, ok := sigcompat.CompatibleSignatureForProvider(sigcompat.SignatureProviderGemini, rawSignature); ok {
thoughtSignature = sig
} else {
thoughtSignature = geminiClaudeThoughtSignature
}
}
part := []byte(`{"text":"","thought":true,"thoughtSignature":""}`)
part, _ = sjson.SetBytes(part, "text", contentResult.Get("thinking").String())
part, _ = sjson.SetBytes(part, "thoughtSignature", contentResult.Get("signature").String())
part, _ = sjson.SetBytes(part, "thoughtSignature", thoughtSignature)
partItems = append(partItems, part)

case "tool_use":
Expand Down
Loading
Loading