Files
CLIProxyAPI/internal/runtime/executor/claude_executor_request.go
sususu afdd251cca fix(claude): preserve semantics in MCP tool aliases
Keep the opaque MCP alias reversible and meaning-preserving so a cloaked
caller's tool declarations, choices and history survive the round trip without
changing tool ownership.
2026-08-03 14:47:26 +08:00

987 lines
32 KiB
Go

package executor
import (
"bufio"
"bytes"
"compress/flate"
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/andybalholm/brotli"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/gin-gonic/gin"
)
const (
defaultClaudeCodeCLIBetas = "claude-code-20250219,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24,fallback-credit-2026-06-01"
claudeTokenCountingBeta = "token-counting-2024-11-01"
claudeFastModeBeta = "fast-mode-2026-02-01"
)
// extractAndRemoveBetas extracts the "betas" array from the body and removes it.
// Returns the extracted betas as a string slice and the modified body.
func extractAndRemoveBetas(body []byte) ([]string, []byte) {
betasResult := gjson.GetBytes(body, "betas")
if !betasResult.Exists() {
return nil, body
}
var betas []string
if betasResult.IsArray() {
for _, item := range betasResult.Array() {
if s := strings.TrimSpace(item.String()); s != "" {
betas = append(betas, s)
}
}
} else if s := strings.TrimSpace(betasResult.String()); s != "" {
betas = append(betas, s)
}
body, _ = sjson.DeleteBytes(body, "betas")
return betas, body
}
func appendClaudeFastModeBeta(body []byte, betas []string) []string {
speed := gjson.GetBytes(body, "speed")
if speed.Type != gjson.String || !strings.EqualFold(strings.TrimSpace(speed.String()), "fast") {
return betas
}
for _, beta := range betas {
if strings.TrimSpace(beta) == claudeFastModeBeta {
return betas
}
}
return append(betas, claudeFastModeBeta)
}
// disableThinkingIfToolChoiceForced checks if tool_choice forces tool use and disables thinking.
// Anthropic API does not allow thinking when tool_choice is set to "any" or a specific tool.
// See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations
func disableThinkingIfToolChoiceForced(body []byte) []byte {
toolChoiceType := gjson.GetBytes(body, "tool_choice.type").String()
// "auto" is allowed with thinking, but "any" or "tool" (specific tool) are not
if toolChoiceType == "any" || toolChoiceType == "tool" {
// Remove thinking configuration entirely to avoid API error
body, _ = sjson.DeleteBytes(body, "thinking")
// Adaptive thinking may also set output_config.effort; remove it to avoid
// leaking thinking controls when tool_choice forces tool use.
body, _ = sjson.DeleteBytes(body, "output_config.effort")
if oc := gjson.GetBytes(body, "output_config"); oc.Exists() && oc.IsObject() && len(oc.Map()) == 0 {
body, _ = sjson.DeleteBytes(body, "output_config")
}
}
return body
}
// normalizeClaudeSamplingForUpstream keeps Anthropic message requests valid.
func normalizeClaudeSamplingForUpstream(body []byte) []byte {
body, _ = sjson.DeleteBytes(body, "temperature")
body, _ = sjson.DeleteBytes(body, "top_p")
thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String()))
switch thinkingType {
case "enabled", "adaptive", "auto":
body, _ = sjson.DeleteBytes(body, "top_p")
body, _ = sjson.DeleteBytes(body, "top_k")
}
return body
}
type compositeReadCloser struct {
io.Reader
closers []func() error
}
func (c *compositeReadCloser) Close() error {
var firstErr error
for i := range c.closers {
if c.closers[i] == nil {
continue
}
if err := c.closers[i](); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
// peekableBody wraps a bufio.Reader around the original ReadCloser so that
// magic bytes can be inspected without consuming them from the stream.
type peekableBody struct {
*bufio.Reader
closer io.Closer
}
func (p *peekableBody) Close() error {
return p.closer.Close()
}
func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) {
if body == nil {
return nil, fmt.Errorf("response body is nil")
}
if contentEncoding == "" {
// No Content-Encoding header. Attempt best-effort magic-byte detection to
// handle misbehaving upstreams that compress without setting the header.
// Only gzip (1f 8b) and zstd (28 b5 2f fd) have reliable magic sequences;
// br and deflate have none and are left as-is.
// The bufio wrapper preserves unread bytes so callers always see the full
// stream regardless of whether decompression was applied.
pb := &peekableBody{Reader: bufio.NewReader(body), closer: body}
magic, peekErr := pb.Peek(4)
if peekErr == nil || (peekErr == io.EOF && len(magic) >= 2) {
switch {
case len(magic) >= 2 && magic[0] == 0x1f && magic[1] == 0x8b:
gzipReader, gzErr := gzip.NewReader(pb)
if gzErr != nil {
_ = pb.Close()
return nil, fmt.Errorf("magic-byte gzip: failed to create reader: %w", gzErr)
}
return &compositeReadCloser{
Reader: gzipReader,
closers: []func() error{
gzipReader.Close,
pb.Close,
},
}, nil
case len(magic) >= 4 && magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd:
decoder, zdErr := zstd.NewReader(pb)
if zdErr != nil {
_ = pb.Close()
return nil, fmt.Errorf("magic-byte zstd: failed to create reader: %w", zdErr)
}
return &compositeReadCloser{
Reader: decoder,
closers: []func() error{
func() error { decoder.Close(); return nil },
pb.Close,
},
}, nil
}
}
return pb, nil
}
encodings := strings.Split(contentEncoding, ",")
for _, raw := range encodings {
encoding := strings.TrimSpace(strings.ToLower(raw))
switch encoding {
case "", "identity":
continue
case "gzip":
gzipReader, err := gzip.NewReader(body)
if err != nil {
_ = body.Close()
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
}
return &compositeReadCloser{
Reader: gzipReader,
closers: []func() error{
gzipReader.Close,
func() error { return body.Close() },
},
}, nil
case "deflate":
deflateReader := flate.NewReader(body)
return &compositeReadCloser{
Reader: deflateReader,
closers: []func() error{
deflateReader.Close,
func() error { return body.Close() },
},
}, nil
case "br":
return &compositeReadCloser{
Reader: brotli.NewReader(body),
closers: []func() error{
func() error { return body.Close() },
},
}, nil
case "zstd":
decoder, err := zstd.NewReader(body)
if err != nil {
_ = body.Close()
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
}
return &compositeReadCloser{
Reader: decoder,
closers: []func() error{
func() error { decoder.Close(); return nil },
func() error { return body.Close() },
},
}, nil
default:
continue
}
}
return body, nil
}
func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool, extraBetas []string, cfg *config.Config, incomingHeaders http.Header, confirmedClaudeCode bool, sessionIDs ...string) error {
if r == nil {
return nil
}
hdrDefault := func(cfgVal, fallback string) string {
if cfgVal != "" {
return cfgVal
}
return fallback
}
var hd config.ClaudeHeaderDefaults
if cfg != nil {
hd = cfg.ClaudeHeaderDefaults
}
hasAPIKeyAttr := auth != nil && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["api_key"]) != ""
oauthToken := isClaudeOAuthToken(apiKey) || !hasAPIKeyAttr
useAPIKey := !oauthToken
isAnthropicBase := r.URL != nil && strings.EqualFold(r.URL.Scheme, "https") && strings.EqualFold(r.URL.Host, "api.anthropic.com")
if isAnthropicBase && useAPIKey {
r.Header.Del("Authorization")
r.Header.Set("x-api-key", apiKey)
} else {
r.Header.Set("Authorization", "Bearer "+apiKey)
}
r.Header.Set("Content-Type", "application/json")
if incomingHeaders == nil {
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
incomingHeaders = ginCtx.Request.Header
}
}
stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg)
var deviceProfile helps.ClaudeDeviceProfile
if stabilizeDeviceProfile && confirmedClaudeCode {
var errDeviceProfile error
deviceProfile, errDeviceProfile = helps.ResolveClaudeDeviceProfileRequired(r.Context(), auth, apiKey, incomingHeaders, cfg)
if errDeviceProfile != nil {
return errDeviceProfile
}
}
baseBetas := defaultClaudeCodeCLIBetas
incomingBetas := strings.TrimSpace(strings.Join(incomingHeaders.Values("Anthropic-Beta"), ","))
if confirmedClaudeCode && incomingBetas != "" {
baseBetas = incomingBetas
}
existingSet := make(map[string]bool)
for _, beta := range strings.Split(baseBetas, ",") {
if beta = strings.TrimSpace(beta); beta != "" {
existingSet[beta] = true
}
}
appendBeta := func(beta string) {
beta = strings.TrimSpace(beta)
if beta == "" || existingSet[beta] {
return
}
baseBetas += "," + beta
existingSet[beta] = true
}
if !confirmedClaudeCode && incomingBetas != "" {
for _, beta := range strings.Split(incomingBetas, ",") {
appendBeta(beta)
}
}
if oauthToken {
appendBeta("oauth-2025-04-20")
}
for _, beta := range extraBetas {
appendBeta(beta)
}
r.Header.Set("Anthropic-Beta", baseBetas)
identityHeader := func(name, fallback string) {
if confirmedClaudeCode {
misc.EnsureHeader(r.Header, incomingHeaders, name, fallback)
return
}
r.Header.Set(name, fallback)
}
identityHeader("Anthropic-Version", "2023-06-01")
identityHeader("Anthropic-Dangerous-Direct-Browser-Access", "true")
identityHeader("X-App", "cli")
// Values below match Claude Code 2.1.220 / @anthropic-ai/sdk 0.94.0.
identityHeader("X-Stainless-Retry-Count", "0")
identityHeader("X-Stainless-Runtime", "node")
identityHeader("X-Stainless-Lang", "js")
identityHeader("X-Stainless-Timeout", hdrDefault(hd.Timeout, "600"))
// Selected-credential OAuth identity is an explicit native passthrough
// exception. Callers pass the same agent-conversation UUID written to
// metadata.user_id; legacy paths retain their previous cached fallback.
sessionID := ""
for _, candidate := range sessionIDs {
if candidate = strings.TrimSpace(candidate); candidate != "" {
sessionID = candidate
break
}
}
if sessionID != "" {
r.Header.Set("X-Claude-Code-Session-Id", sessionID)
} else {
var errSessionID error
sessionID, errSessionID = helps.CachedSessionIDRequired(r.Context(), apiKey)
if errSessionID != nil {
return errSessionID
}
identityHeader("X-Claude-Code-Session-Id", sessionID)
}
// Per-request UUID, matches Claude Code's x-client-request-id for first-party API.
if isAnthropicBase {
identityHeader("x-client-request-id", uuid.New().String())
}
r.Header.Set("Connection", "keep-alive")
if stream {
r.Header.Set("Accept", "text/event-stream")
// SSE streams must not be compressed: the downstream scanner reads
// line-delimited text and cannot parse compressed bytes. Using
// "identity" tells the upstream to send an uncompressed stream.
r.Header.Set("Accept-Encoding", "identity")
} else {
r.Header.Set("Accept", "application/json")
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
}
// Confirmed Claude Code requests may contribute their real software profile.
// Unconfirmed clients always receive the CLI baseline instead of being
// allowed to populate or reuse another client's software profile.
if stabilizeDeviceProfile {
if confirmedClaudeCode {
helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)
} else {
helps.ApplyClaudeDefaultDeviceProfileHeaders(r, cfg)
}
} else {
helps.ApplyClaudeLegacyDeviceHeaders(r, incomingHeaders, cfg, confirmedClaudeCode)
}
var attrs map[string]string
if auth != nil {
attrs = auth.Attributes
}
util.ApplyCustomHeadersFromAttrs(r, attrs)
// Re-enforce the SSE transport contract after custom headers. A custom Accept
// value can disable event negotiation, while compressed SSE breaks line parsing.
if stream {
r.Header.Set("Accept", "text/event-stream")
r.Header.Set("Accept-Encoding", "identity")
}
return nil
}
func claudeCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
if a == nil {
return "", ""
}
if a.Attributes != nil {
apiKey = a.Attributes["api_key"]
baseURL = a.Attributes["base_url"]
}
if apiKey == "" && a.Metadata != nil {
if v, ok := a.Metadata["access_token"].(string); ok {
apiKey = v
}
}
return
}
func rebuildMidSystemMessagesToTopLevel(payload []byte) []byte {
messages := gjson.GetBytes(payload, "messages")
if !messages.IsArray() {
return payload
}
var movedSystemParts []string
keptMessages := make([]string, 0, int(messages.Get("#").Int()))
messages.ForEach(func(_, message gjson.Result) bool {
if strings.EqualFold(strings.TrimSpace(message.Get("role").String()), "system") {
movedSystemParts = append(movedSystemParts, claudeSystemTextParts(message.Get("content"))...)
return true
}
keptMessages = append(keptMessages, message.Raw)
return true
})
if len(movedSystemParts) == 0 {
return payload
}
systemParts := claudeSystemTextParts(gjson.GetBytes(payload, "system"))
systemParts = append(systemParts, movedSystemParts...)
if len(systemParts) > 0 {
if updated, errSetSystem := sjson.SetRawBytes(payload, "system", rawJSONArray(systemParts)); errSetSystem == nil {
payload = updated
}
}
if updated, errSetMessages := sjson.SetRawBytes(payload, "messages", rawJSONArray(keptMessages)); errSetMessages == nil {
payload = updated
}
return payload
}
func claudeSystemTextParts(content gjson.Result) []string {
if !content.Exists() {
return nil
}
if content.Type == gjson.String {
text := content.String()
if strings.TrimSpace(text) == "" {
return nil
}
block := []byte(`{"type":"text","text":""}`)
block, _ = sjson.SetBytes(block, "text", text)
return []string{string(block)}
}
if !content.IsArray() {
return nil
}
var parts []string
content.ForEach(func(_, item gjson.Result) bool {
if item.Type == gjson.String {
text := item.String()
if strings.TrimSpace(text) != "" {
block := []byte(`{"type":"text","text":""}`)
block, _ = sjson.SetBytes(block, "text", text)
parts = append(parts, string(block))
}
return true
}
if item.IsObject() && item.Get("type").String() == "text" && strings.TrimSpace(item.Get("text").String()) != "" {
parts = append(parts, item.Raw)
}
return true
})
return parts
}
func rawJSONArray(items []string) []byte {
if len(items) == 0 {
return []byte("[]")
}
var builder strings.Builder
builder.WriteByte('[')
for i, item := range items {
if i > 0 {
builder.WriteByte(',')
}
builder.WriteString(item)
}
builder.WriteByte(']')
return []byte(builder.String())
}
func isClaudeOAuthToken(apiKey string) bool {
return strings.Contains(apiKey, "sk-ant-oat")
}
type claudeMCPAliasOptions struct {
secret string
}
func resolveClaudeMCPAliasOptions(ctx context.Context) claudeMCPAliasOptions {
// Alias identity belongs to the downstream caller, not to the selected
// upstream credential. This keeps names stable across OAuth refresh and auth
// failover while giving one caller a shared virtual MCP server component.
secret := strings.TrimSpace(helps.APIKeyFromContext(ctx))
if secret == "" {
secret = "cpa-claude-mcp-default-caller"
}
return claudeMCPAliasOptions{secret: secret}
}
// prepareClaudeOAuthToolNamesForUpstream applies one request-local MCP symbol
// table across every Claude OAuth request path.
func prepareClaudeOAuthToolNamesForUpstream(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string) {
return remapOAuthToolNamesWithOptions(body, mcpAliases)
}
func restoreClaudeOAuthToolNamesFromResponse(body []byte, reverseMap map[string]string) []byte {
return reverseRemapOAuthToolNames(body, reverseMap)
}
func restoreClaudeOAuthToolNamesFromStreamLine(line []byte, reverseMap map[string]string) []byte {
return reverseRemapOAuthToolNamesFromStreamLine(line, reverseMap)
}
// remapOAuthToolNames represents every declared third-party client tool as a
// semantic Claude Code MCP extension. Existing valid MCP names and explicit
// typed Anthropic tools remain unchanged.
//
// It operates on tools[].name, tool_choice.name, and all declared
// tool_use/tool_reference references in messages.
//
// The returned map is keyed on the upstream name and maps to the client-supplied
// original name. Callers MUST pass this map to the reverse
// functions so only aliases allocated for this request are restored on the
// response. A global reverse map would mix symbols from unrelated callers.
func remapOAuthToolNames(body []byte) ([]byte, map[string]string) {
return remapOAuthToolNamesWithOptions(body, claudeMCPAliasOptions{secret: "cpa-claude-mcp-default-caller"})
}
func remapOAuthToolNamesWithOptions(body []byte, mcpAliases claudeMCPAliasOptions) ([]byte, map[string]string) {
reverseMap := make(map[string]string)
recordRename := func(original, renamed string) {
// Preserve the first-seen original name if the same upstream name is
// produced from multiple call sites; they all map back identically.
if _, exists := reverseMap[renamed]; !exists {
reverseMap[renamed] = original
}
}
// Build one request-specific forward map from declarations. Every client
// tool, including typed custom declarations and names resembling Claude
// built-ins, gets an MCP alias. Historical references use this same map.
tools := gjson.GetBytes(body, "tools")
forwardMap := make(map[string]string)
protectedNames := make(map[string]bool)
reservedNames := helps.AugmentClaudeBuiltinToolRegistry(body, nil)
if tools.Exists() && tools.IsArray() {
tools.ForEach(func(_, tool gjson.Result) bool {
name := tool.Get("name").String()
if name != "" {
reservedNames[name] = true
}
if helps.IsClaudeServerToolType(tool.Get("type").String()) {
protectedNames[name] = true
}
return true
})
tools.ForEach(func(_, tool gjson.Result) bool {
if helps.IsClaudeServerToolType(tool.Get("type").String()) {
return true
}
name := tool.Get("name").String()
if name == "" || helps.IsClaudeMCPToolName(name) {
return true
}
if _, exists := forwardMap[name]; exists {
return true
}
for attempt := uint32(0); ; attempt++ {
alias := helps.ClaudeMCPToolAlias(mcpAliases.secret, name, attempt)
if reservedNames[alias] {
continue
}
forwardMap[name] = alias
reservedNames[alias] = true
break
}
return true
})
}
rewriteName := func(name string) (string, bool) {
if name == "" || protectedNames[name] || helps.IsClaudeMCPToolName(name) {
return name, false
}
if newName, ok := forwardMap[name]; ok && newName != name {
return newName, true
}
return name, false
}
// 1. Rewrite the tools array without rebuilding from a stale gjson snapshot.
toolsNeedRewrite := false
if tools.Exists() && tools.IsArray() {
tools.ForEach(func(_, tool gjson.Result) bool {
toolType := tool.Get("type").String()
if helps.IsClaudeServerToolType(toolType) {
return true
}
if strings.TrimSpace(toolType) != "" {
toolsNeedRewrite = true
return false
}
name := tool.Get("name").String()
_, toolsNeedRewrite = rewriteName(name)
return !toolsNeedRewrite
})
}
if toolsNeedRewrite {
var toolsJSON strings.Builder
toolsJSON.WriteByte('[')
toolCount := 0
tools.ForEach(func(_, tool gjson.Result) bool {
if helps.IsClaudeServerToolType(tool.Get("type").String()) {
if toolCount > 0 {
toolsJSON.WriteByte(',')
}
toolsJSON.WriteString(tool.Raw)
toolCount++
return true
}
name := tool.Get("name").String()
toolJSON := tool.Raw
if strings.TrimSpace(tool.Get("type").String()) != "" {
if updatedTool, errDelete := sjson.Delete(toolJSON, "type"); errDelete == nil {
toolJSON = updatedTool
}
}
if newName, renamed := rewriteName(name); renamed {
updatedTool, err := sjson.Set(toolJSON, "name", newName)
if err == nil {
toolJSON = updatedTool
recordRename(name, newName)
}
}
if toolCount > 0 {
toolsJSON.WriteByte(',')
}
toolsJSON.WriteString(toolJSON)
toolCount++
return true
})
toolsJSON.WriteByte(']')
body, _ = sjson.SetRawBytes(body, "tools", []byte(toolsJSON.String()))
}
// 2. Rename tool_choice if it references a declared client tool.
toolChoiceType := gjson.GetBytes(body, "tool_choice.type").String()
if toolChoiceType == "tool" {
tcName := gjson.GetBytes(body, "tool_choice.name").String()
if newName, renamed := rewriteName(tcName); renamed {
body, _ = sjson.SetBytes(body, "tool_choice.name", newName)
recordRename(tcName, newName)
}
}
// 3. Rename tool references in messages
messages := gjson.GetBytes(body, "messages")
if messages.Exists() && messages.IsArray() {
messages.ForEach(func(msgIndex, msg gjson.Result) bool {
content := msg.Get("content")
if !content.Exists() || !content.IsArray() {
return true
}
content.ForEach(func(contentIndex, part gjson.Result) bool {
partType := part.Get("type").String()
switch partType {
case "tool_use":
name := part.Get("name").String()
if newName, renamed := rewriteName(name); renamed {
path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int())
body, _ = sjson.SetBytes(body, path, newName)
recordRename(name, newName)
}
case "tool_reference":
toolName := part.Get("tool_name").String()
if newName, renamed := rewriteName(toolName); renamed {
path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int())
body, _ = sjson.SetBytes(body, path, newName)
recordRename(toolName, newName)
}
case "tool_result":
// Handle nested tool_reference blocks inside tool_result.content[]
toolID := part.Get("tool_use_id").String()
_ = toolID // tool_use_id stays as-is
nestedContent := part.Get("content")
if nestedContent.Exists() && nestedContent.IsArray() {
nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool {
if nestedPart.Get("type").String() == "tool_reference" {
nestedToolName := nestedPart.Get("tool_name").String()
if newName, renamed := rewriteName(nestedToolName); renamed {
nestedPath := fmt.Sprintf("messages.%d.content.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int(), nestedIndex.Int())
body, _ = sjson.SetBytes(body, nestedPath, newName)
recordRename(nestedToolName, newName)
}
}
return true
})
}
}
return true
})
return true
})
}
return body, reverseMap
}
// reverseRemapOAuthToolNames reverses the tool name mapping for non-stream responses
// using the per-request map produced by remapOAuthToolNames. Names the client sent
// that were NOT forward-renamed are passed through unchanged.
func reverseRemapOAuthToolNames(body []byte, reverseMap map[string]string) []byte {
if len(reverseMap) == 0 {
return body
}
content := gjson.GetBytes(body, "content")
if !content.Exists() || !content.IsArray() {
return body
}
content.ForEach(func(index, part gjson.Result) bool {
partType := part.Get("type").String()
switch partType {
case "tool_use":
name := part.Get("name").String()
if origName, ok := reverseMap[name]; ok {
path := fmt.Sprintf("content.%d.name", index.Int())
body, _ = sjson.SetBytes(body, path, origName)
}
case "tool_reference":
toolName := part.Get("tool_name").String()
if origName, ok := reverseMap[toolName]; ok {
path := fmt.Sprintf("content.%d.tool_name", index.Int())
body, _ = sjson.SetBytes(body, path, origName)
}
case "tool_result":
nestedContent := part.Get("content")
if nestedContent.Exists() && nestedContent.IsArray() {
nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool {
if nestedPart.Get("type").String() != "tool_reference" {
return true
}
toolName := nestedPart.Get("tool_name").String()
if origName, ok := reverseMap[toolName]; ok {
path := fmt.Sprintf("content.%d.content.%d.tool_name", index.Int(), nestedIndex.Int())
body, _ = sjson.SetBytes(body, path, origName)
}
return true
})
}
}
return true
})
return body
}
// reverseRemapOAuthToolNamesFromStreamLine reverses the tool name mapping for SSE
// stream lines, using the per-request reverseMap produced by remapOAuthToolNames.
func reverseRemapOAuthToolNamesFromStreamLine(line []byte, reverseMap map[string]string) []byte {
if len(reverseMap) == 0 {
return line
}
payload := helps.JSONPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) {
return line
}
contentBlock := gjson.GetBytes(payload, "content_block")
if !contentBlock.Exists() {
return line
}
blockType := contentBlock.Get("type").String()
var updated []byte
var err error
switch blockType {
case "tool_use":
name := contentBlock.Get("name").String()
if origName, ok := reverseMap[name]; ok {
updated, err = sjson.SetBytes(payload, "content_block.name", origName)
if err != nil {
return line
}
} else {
return line
}
case "tool_reference":
toolName := contentBlock.Get("tool_name").String()
if origName, ok := reverseMap[toolName]; ok {
updated, err = sjson.SetBytes(payload, "content_block.tool_name", origName)
if err != nil {
return line
}
} else {
return line
}
default:
return line
}
trimmed := bytes.TrimSpace(line)
if bytes.HasPrefix(trimmed, []byte("data:")) {
return append([]byte("data: "), updated...)
}
return updated
}
func applyClaudeToolPrefix(body []byte, prefix string) []byte {
if prefix == "" {
return body
}
// Collect built-in tool names from the authoritative fallback seed list and
// augment it with any typed built-ins present in the current request body.
builtinTools := helps.AugmentClaudeBuiltinToolRegistry(body, nil)
if tools := gjson.GetBytes(body, "tools"); tools.Exists() && tools.IsArray() {
tools.ForEach(func(index, tool gjson.Result) bool {
// Skip built-in tools (web_search, code_execution, etc.) which have
// a "type" field and require their name to remain unchanged.
if tool.Get("type").Exists() && tool.Get("type").String() != "" {
if n := tool.Get("name").String(); n != "" {
builtinTools[n] = true
}
return true
}
name := tool.Get("name").String()
if name == "" || strings.HasPrefix(name, prefix) || helps.IsClaudeMCPToolName(name) {
return true
}
path := fmt.Sprintf("tools.%d.name", index.Int())
body, _ = sjson.SetBytes(body, path, prefix+name)
return true
})
}
if gjson.GetBytes(body, "tool_choice.type").String() == "tool" {
name := gjson.GetBytes(body, "tool_choice.name").String()
if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] && !helps.IsClaudeMCPToolName(name) {
body, _ = sjson.SetBytes(body, "tool_choice.name", prefix+name)
}
}
if messages := gjson.GetBytes(body, "messages"); messages.Exists() && messages.IsArray() {
messages.ForEach(func(msgIndex, msg gjson.Result) bool {
content := msg.Get("content")
if !content.Exists() || !content.IsArray() {
return true
}
content.ForEach(func(contentIndex, part gjson.Result) bool {
partType := part.Get("type").String()
switch partType {
case "tool_use":
name := part.Get("name").String()
if name == "" || strings.HasPrefix(name, prefix) || builtinTools[name] || helps.IsClaudeMCPToolName(name) {
return true
}
path := fmt.Sprintf("messages.%d.content.%d.name", msgIndex.Int(), contentIndex.Int())
body, _ = sjson.SetBytes(body, path, prefix+name)
case "tool_reference":
toolName := part.Get("tool_name").String()
if toolName == "" || strings.HasPrefix(toolName, prefix) || builtinTools[toolName] || helps.IsClaudeMCPToolName(toolName) {
return true
}
path := fmt.Sprintf("messages.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int())
body, _ = sjson.SetBytes(body, path, prefix+toolName)
case "tool_result":
// Handle nested tool_reference blocks inside tool_result.content[]
nestedContent := part.Get("content")
if nestedContent.Exists() && nestedContent.IsArray() {
nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool {
if nestedPart.Get("type").String() == "tool_reference" {
nestedToolName := nestedPart.Get("tool_name").String()
if nestedToolName != "" && !strings.HasPrefix(nestedToolName, prefix) && !builtinTools[nestedToolName] && !helps.IsClaudeMCPToolName(nestedToolName) {
nestedPath := fmt.Sprintf("messages.%d.content.%d.content.%d.tool_name", msgIndex.Int(), contentIndex.Int(), nestedIndex.Int())
body, _ = sjson.SetBytes(body, nestedPath, prefix+nestedToolName)
}
}
return true
})
}
}
return true
})
return true
})
}
return body
}
func stripClaudeToolPrefixFromResponse(body []byte, prefix string) []byte {
if prefix == "" {
return body
}
content := gjson.GetBytes(body, "content")
if !content.Exists() || !content.IsArray() {
return body
}
content.ForEach(func(index, part gjson.Result) bool {
partType := part.Get("type").String()
switch partType {
case "tool_use":
name := part.Get("name").String()
if !strings.HasPrefix(name, prefix) {
return true
}
path := fmt.Sprintf("content.%d.name", index.Int())
body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(name, prefix))
case "tool_reference":
toolName := part.Get("tool_name").String()
if !strings.HasPrefix(toolName, prefix) {
return true
}
path := fmt.Sprintf("content.%d.tool_name", index.Int())
body, _ = sjson.SetBytes(body, path, strings.TrimPrefix(toolName, prefix))
case "tool_result":
// Handle nested tool_reference blocks inside tool_result.content[]
nestedContent := part.Get("content")
if nestedContent.Exists() && nestedContent.IsArray() {
nestedContent.ForEach(func(nestedIndex, nestedPart gjson.Result) bool {
if nestedPart.Get("type").String() == "tool_reference" {
nestedToolName := nestedPart.Get("tool_name").String()
if strings.HasPrefix(nestedToolName, prefix) {
nestedPath := fmt.Sprintf("content.%d.content.%d.tool_name", index.Int(), nestedIndex.Int())
body, _ = sjson.SetBytes(body, nestedPath, strings.TrimPrefix(nestedToolName, prefix))
}
}
return true
})
}
}
return true
})
return body
}
func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte {
if prefix == "" {
return line
}
payload := helps.JSONPayload(line)
if len(payload) == 0 || !gjson.ValidBytes(payload) {
return line
}
contentBlock := gjson.GetBytes(payload, "content_block")
if !contentBlock.Exists() {
return line
}
blockType := contentBlock.Get("type").String()
var updated []byte
var err error
switch blockType {
case "tool_use":
name := contentBlock.Get("name").String()
if !strings.HasPrefix(name, prefix) {
return line
}
updated, err = sjson.SetBytes(payload, "content_block.name", strings.TrimPrefix(name, prefix))
if err != nil {
return line
}
case "tool_reference":
toolName := contentBlock.Get("tool_name").String()
if !strings.HasPrefix(toolName, prefix) {
return line
}
updated, err = sjson.SetBytes(payload, "content_block.tool_name", strings.TrimPrefix(toolName, prefix))
if err != nil {
return line
}
default:
return line
}
trimmed := bytes.TrimSpace(line)
if bytes.HasPrefix(trimmed, []byte("data:")) {
return append([]byte("data: "), updated...)
}
return updated
}