Files
clawgo/pkg/providers/claude_provider.go

1733 lines
51 KiB
Go

package providers
import (
"bufio"
"bytes"
"compress/flate"
"compress/gzip"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"sort"
"strings"
"time"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd"
)
const claudeBaseURL = "https://api.anthropic.com"
const claudeToolPrefix = ""
type ClaudeProvider struct {
base *HTTPProvider
}
func NewClaudeProvider(providerName, apiKey, apiBase, defaultModel string, supportsResponsesCompact bool, authMode string, timeout time.Duration, oauth *oauthManager) *ClaudeProvider {
return &ClaudeProvider{
base: NewHTTPProvider(providerName, apiKey, apiBase, defaultModel, supportsResponsesCompact, authMode, timeout, oauth),
}
}
func (p *ClaudeProvider) GetDefaultModel() string {
if p == nil || p.base == nil {
return ""
}
return p.base.GetDefaultModel()
}
func (p *ClaudeProvider) CountTokens(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*UsageInfo, error) {
if p == nil || p.base == nil {
return nil, fmt.Errorf("provider not configured")
}
body, statusCode, contentType, err := p.countTokens(ctx, p.countTokensRequestBody(messages, tools, model, options), options)
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body))
}
var payload struct {
InputTokens int `json:"input_tokens"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, fmt.Errorf("invalid count_tokens response: %w", err)
}
return &UsageInfo{
PromptTokens: payload.InputTokens,
TotalTokens: payload.InputTokens,
}, nil
}
func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
if p == nil || p.base == nil {
return nil, fmt.Errorf("provider not configured")
}
body, statusCode, contentType, err := p.postJSON(ctx, p.requestBody(messages, tools, model, options, false), options)
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body))
}
if !json.Valid(body) {
return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body))
}
body = stripClaudeToolPrefixFromResponse(body, claudeToolPrefix)
return parseClaudeResponse(body)
}
func (p *ClaudeProvider) ChatStream(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, onDelta func(string)) (*LLMResponse, error) {
if p == nil || p.base == nil {
return nil, fmt.Errorf("provider not configured")
}
if onDelta == nil {
onDelta = func(string) {}
}
body, statusCode, contentType, err := p.stream(ctx, p.requestBody(messages, tools, model, options, true), options, onDelta)
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body))
}
if !json.Valid(body) {
return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body))
}
body = stripClaudeToolPrefixFromResponse(body, claudeToolPrefix)
return parseClaudeResponse(body)
}
func (p *ClaudeProvider) baseURL() string {
if p == nil || p.base == nil {
return claudeBaseURL
}
base := strings.TrimSpace(p.base.apiBase)
if base == "" || strings.Contains(strings.ToLower(base), "api.openai.com") {
return claudeBaseURL
}
return normalizeAPIBase(base)
}
func (p *ClaudeProvider) requestBody(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, stream bool) map[string]interface{} {
systemParts := make([]string, 0)
outMessages := make([]map[string]interface{}, 0, len(messages))
callNames := map[string]string{}
for _, msg := range messages {
role := strings.ToLower(strings.TrimSpace(msg.Role))
switch role {
case "system", "developer":
if text := claudeTextParts(msg.ContentParts); text != "" {
systemParts = append(systemParts, text)
}
if text := strings.TrimSpace(msg.Content); text != "" {
systemParts = append(systemParts, text)
}
case "assistant":
content := make([]map[string]interface{}, 0, 1+len(msg.ToolCalls))
if text := strings.TrimSpace(msg.Content); text != "" {
content = append(content, map[string]interface{}{"type": "text", "text": text})
}
for _, tc := range msg.ToolCalls {
name := strings.TrimSpace(tc.Name)
if tc.Function != nil && strings.TrimSpace(tc.Function.Name) != "" {
name = strings.TrimSpace(tc.Function.Name)
}
if name == "" {
continue
}
input := map[string]interface{}{}
if tc.Function != nil && strings.TrimSpace(tc.Function.Arguments) != "" {
_ = json.Unmarshal([]byte(tc.Function.Arguments), &input)
}
if len(input) == 0 && len(tc.Arguments) > 0 {
input = tc.Arguments
}
if strings.TrimSpace(tc.ID) != "" {
callNames[strings.TrimSpace(tc.ID)] = name
}
content = append(content, map[string]interface{}{
"type": "tool_use",
"id": tc.ID,
"name": name,
"input": input,
})
}
if len(content) == 1 && len(msg.ToolCalls) == 0 && strings.EqualFold(asString(content[0]["type"]), "text") {
outMessages = append(outMessages, map[string]interface{}{"role": "assistant", "content": asString(content[0]["text"])})
continue
}
if len(content) > 0 {
outMessages = append(outMessages, map[string]interface{}{"role": "assistant", "content": content})
}
case "tool":
callID := strings.TrimSpace(msg.ToolCallID)
if callID == "" {
continue
}
toolResult := map[string]interface{}{
"type": "tool_result",
"tool_use_id": callID,
}
if content := claudeToolResultContent(msg); content != nil {
toolResult["content"] = content
} else {
toolResult["content"] = strings.TrimSpace(msg.Content)
}
if name := strings.TrimSpace(callNames[callID]); name != "" {
toolResult["tool_name"] = name
}
outMessages = append(outMessages, map[string]interface{}{"role": "user", "content": []map[string]interface{}{toolResult}})
default:
content := claudeContentPartsForMessage(msg)
if len(content) == 0 && strings.TrimSpace(msg.Content) != "" {
outMessages = append(outMessages, map[string]interface{}{"role": "user", "content": strings.TrimSpace(msg.Content)})
continue
}
if len(content) == 1 && strings.EqualFold(asString(content[0]["type"]), "text") {
outMessages = append(outMessages, map[string]interface{}{"role": "user", "content": asString(content[0]["text"])})
continue
}
if len(content) > 0 {
outMessages = append(outMessages, map[string]interface{}{"role": "user", "content": content})
}
}
}
body := map[string]interface{}{
"model": strings.TrimSpace(model),
"messages": outMessages,
"stream": stream,
}
if len(systemParts) > 0 {
system := make([]map[string]interface{}, 0, len(systemParts))
for _, text := range systemParts {
text = strings.TrimSpace(text)
if text == "" {
continue
}
system = append(system, map[string]interface{}{
"type": "text",
"text": text,
})
}
body["system"] = system
}
if maxTokens, ok := int64FromOption(options, "max_tokens"); ok && maxTokens > 0 {
body["max_tokens"] = maxTokens
} else {
body["max_tokens"] = int64(4096)
}
if temperature, ok := float64FromOption(options, "temperature"); ok {
body["temperature"] = temperature
}
if len(tools) > 0 {
toolDefs := make([]map[string]interface{}, 0, len(tools))
for _, tool := range tools {
name := strings.TrimSpace(tool.Function.Name)
if name == "" {
name = strings.TrimSpace(tool.Name)
}
if name == "" {
continue
}
schema := tool.Function.Parameters
if len(schema) == 0 {
schema = tool.Parameters
}
if len(schema) == 0 {
schema = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}
}
toolDefs = append(toolDefs, map[string]interface{}{
"name": name,
"description": strings.TrimSpace(firstNonEmpty(tool.Function.Description, tool.Description)),
"input_schema": schema,
})
}
if len(toolDefs) > 0 {
body["tools"] = toolDefs
body["tool_choice"] = map[string]interface{}{"type": "auto"}
}
}
if toolChoice := claudeToolChoice(options); len(toolChoice) > 0 {
body["tool_choice"] = toolChoice
}
if thinking, ok := mapOption(options, "thinking"); ok && len(thinking) > 0 {
body["thinking"] = thinking
}
body = enrichClaudeSystemBlocks(body, claudeStrictSystemEnabled(options))
body = disableClaudeThinkingIfToolChoiceForced(body)
body = ensureClaudeCacheControl(body)
body = enforceClaudeCacheControlLimit(body, 4)
body = normalizeClaudeCacheControlTTL(body)
body = applyClaudeToolPrefixToBody(body, claudeToolPrefix)
return body
}
func (p *ClaudeProvider) countTokensRequestBody(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) map[string]interface{} {
body := p.requestBody(messages, tools, model, options, false)
delete(body, "stream")
delete(body, "max_tokens")
return body
}
func claudeContentPartsForMessage(msg Message) []map[string]interface{} {
if len(msg.ContentParts) == 0 {
return nil
}
content := make([]map[string]interface{}, 0, len(msg.ContentParts))
for _, part := range msg.ContentParts {
if converted := claudeContentPartFromMessagePart(part); len(converted) > 0 {
content = append(content, converted)
}
}
return content
}
func claudeTextParts(parts []MessageContentPart) string {
if len(parts) == 0 {
return ""
}
texts := make([]string, 0, len(parts))
for _, part := range parts {
switch strings.ToLower(strings.TrimSpace(part.Type)) {
case "text", "input_text":
if text := strings.TrimSpace(part.Text); text != "" {
texts = append(texts, text)
}
}
}
return strings.TrimSpace(strings.Join(texts, "\n"))
}
func claudeContentPartFromMessagePart(part MessageContentPart) map[string]interface{} {
switch strings.ToLower(strings.TrimSpace(part.Type)) {
case "text", "input_text":
if text := strings.TrimSpace(part.Text); text != "" {
return map[string]interface{}{"type": "text", "text": text}
}
case "image", "input_image":
return claudeImagePart(part)
case "file", "input_file":
return claudeDocumentPart(part)
}
return nil
}
func claudeToolResultContent(msg Message) interface{} {
if len(msg.ContentParts) == 0 {
return nil
}
content := make([]map[string]interface{}, 0, len(msg.ContentParts))
for _, part := range msg.ContentParts {
if converted := claudeContentPartFromMessagePart(part); len(converted) > 0 {
content = append(content, converted)
}
}
if len(content) == 0 {
return nil
}
return content
}
func claudeImagePart(part MessageContentPart) map[string]interface{} {
imageURL := strings.TrimSpace(part.ImageURL)
if imageURL == "" {
return nil
}
if strings.HasPrefix(imageURL, "data:") {
mediaType, data := parseClaudeDataURL(imageURL, "application/octet-stream")
if data == "" {
return nil
}
return map[string]interface{}{
"type": "image",
"source": map[string]interface{}{
"type": "base64",
"media_type": mediaType,
"data": data,
},
}
}
return map[string]interface{}{
"type": "image",
"source": map[string]interface{}{
"type": "url",
"url": imageURL,
},
}
}
func claudeDocumentPart(part MessageContentPart) map[string]interface{} {
fileData := strings.TrimSpace(part.FileData)
if fileData == "" {
return nil
}
mediaType, data := parseClaudeDataURL(fileData, firstNonEmpty(strings.TrimSpace(part.MIMEType), "application/octet-stream"))
if data == "" {
return nil
}
return map[string]interface{}{
"type": "document",
"source": map[string]interface{}{
"type": "base64",
"media_type": mediaType,
"data": data,
},
}
}
func parseClaudeDataURL(value string, fallbackMediaType string) (string, string) {
value = strings.TrimSpace(value)
if value == "" {
return fallbackMediaType, ""
}
if !strings.HasPrefix(value, "data:") {
return fallbackMediaType, value
}
trimmed := strings.TrimPrefix(value, "data:")
parts := strings.SplitN(trimmed, ";base64,", 2)
if len(parts) != 2 {
return fallbackMediaType, ""
}
mediaType := strings.TrimSpace(parts[0])
if mediaType == "" {
mediaType = fallbackMediaType
}
return mediaType, strings.TrimSpace(parts[1])
}
func enrichClaudeSystemBlocks(body map[string]interface{}, strict bool) map[string]interface{} {
if body == nil {
return nil
}
systemBlocks := buildClaudeSystemBlocks(body["system"], body, strict)
if len(systemBlocks) == 0 {
return body
}
body["system"] = systemBlocks
return body
}
func buildClaudeSystemBlocks(system interface{}, body map[string]interface{}, strict bool) []map[string]interface{} {
userBlocks := make([]map[string]interface{}, 0)
switch typed := system.(type) {
case string:
if text := strings.TrimSpace(typed); text != "" {
userBlocks = append(userBlocks, map[string]interface{}{
"type": "text",
"text": text,
"cache_control": map[string]interface{}{"type": "ephemeral"},
})
}
case []map[string]interface{}:
for _, item := range typed {
if strings.HasPrefix(strings.TrimSpace(asString(item["text"])), "x-anthropic-billing-header:") {
return typed
}
clone := map[string]interface{}{}
for k, v := range item {
clone[k] = v
}
if strings.EqualFold(asString(clone["type"]), "text") && mapFromAny(clone["cache_control"]) == nil {
if _, exists := clone["cache_control"]; !exists {
clone["cache_control"] = map[string]interface{}{"type": "ephemeral"}
}
}
userBlocks = append(userBlocks, clone)
}
case []interface{}:
for _, raw := range typed {
item := mapFromAny(raw)
if len(item) == 0 {
continue
}
if strings.HasPrefix(strings.TrimSpace(asString(item["text"])), "x-anthropic-billing-header:") {
return claudeMustMapSlice(typed)
}
clone := map[string]interface{}{}
for k, v := range item {
clone[k] = v
}
if strings.EqualFold(asString(clone["type"]), "text") {
if _, exists := clone["cache_control"]; !exists {
clone["cache_control"] = map[string]interface{}{"type": "ephemeral"}
}
}
userBlocks = append(userBlocks, clone)
}
}
systemBlocks := []map[string]interface{}{
{"type": "text", "text": generateClaudeBillingHeader(body)},
{"type": "text", "text": "You are a Claude agent, built on Anthropic's Claude Agent SDK."},
}
if strict {
return systemBlocks
}
if len(userBlocks) == 0 {
return nil
}
systemBlocks = append(systemBlocks, userBlocks...)
return systemBlocks
}
func generateClaudeBillingHeader(body map[string]interface{}) string {
raw, _ := json.Marshal(body)
sum := sha256.Sum256(raw)
cch := hex.EncodeToString(sum[:])[:5]
var buildBytes [2]byte
if _, err := rand.Read(buildBytes[:]); err != nil {
return fmt.Sprintf("x-anthropic-billing-header: cc_version=2.1.63.000; cc_entrypoint=cli; cch=%s;", cch)
}
buildHash := hex.EncodeToString(buildBytes[:])[:3]
return fmt.Sprintf("x-anthropic-billing-header: cc_version=2.1.63.%s; cc_entrypoint=cli; cch=%s;", buildHash, cch)
}
func claudeMustMapSlice(items []interface{}) []map[string]interface{} {
out := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
if obj := mapFromAny(item); len(obj) > 0 {
out = append(out, obj)
}
}
return out
}
func (p *ClaudeProvider) postJSON(ctx context.Context, payload map[string]interface{}, options map[string]interface{}) ([]byte, int, string, error) {
extraBetas, payload := extractClaudeBetasFromPayload(payload)
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err)
}
attempts, err := p.base.authAttempts(ctx)
if err != nil {
return nil, 0, "", err
}
var lastBody []byte
var lastStatus int
var lastType string
for _, attempt := range attempts {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointFor(p.baseURL(), "/v1/messages"), bytes.NewReader(jsonData))
if err != nil {
return nil, 0, "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
applyAttemptAuth(req, attempt)
applyAttemptProviderHeaders(req, attempt, p.base, false)
applyClaudeCompatHeaders(req, attempt, false)
applyClaudeBetaHeaders(req, options, extraBetas)
body, status, ctype, err := p.doJSONAttempt(req, attempt)
if err != nil {
return nil, 0, "", err
}
reason, retry := classifyOAuthFailure(status, body)
if !retry {
p.base.markAttemptSuccess(attempt)
return body, status, ctype, nil
}
lastBody, lastStatus, lastType = body, status, ctype
if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil {
p.base.oauth.markExhausted(attempt.session, reason)
recordProviderOAuthError(p.base.providerName, attempt.session, reason)
}
if attempt.kind == "api_key" {
p.base.markAPIKeyFailure(reason)
}
}
return lastBody, lastStatus, lastType, nil
}
func (p *ClaudeProvider) stream(ctx context.Context, payload map[string]interface{}, options map[string]interface{}, onDelta func(string)) ([]byte, int, string, error) {
extraBetas, payload := extractClaudeBetasFromPayload(payload)
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err)
}
attempts, err := p.base.authAttempts(ctx)
if err != nil {
return nil, 0, "", err
}
var lastBody []byte
var lastStatus int
var lastType string
for _, attempt := range attempts {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointFor(p.baseURL(), "/v1/messages"), bytes.NewReader(jsonData))
if err != nil {
return nil, 0, "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
applyAttemptAuth(req, attempt)
applyAttemptProviderHeaders(req, attempt, p.base, true)
applyClaudeCompatHeaders(req, attempt, true)
applyClaudeBetaHeaders(req, options, extraBetas)
body, status, ctype, quotaHit, err := p.consumeClaudeStream(req, attempt, onDelta)
if err != nil {
return nil, 0, "", err
}
if !quotaHit {
p.base.markAttemptSuccess(attempt)
return body, status, ctype, nil
}
lastBody, lastStatus, lastType = body, status, ctype
if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil {
reason, _ := classifyOAuthFailure(status, body)
p.base.oauth.markExhausted(attempt.session, reason)
recordProviderOAuthError(p.base.providerName, attempt.session, reason)
}
if attempt.kind == "api_key" {
reason, _ := classifyOAuthFailure(status, body)
p.base.markAPIKeyFailure(reason)
}
}
return lastBody, lastStatus, lastType, nil
}
func (p *ClaudeProvider) countTokens(ctx context.Context, payload map[string]interface{}, options map[string]interface{}) ([]byte, int, string, error) {
extraBetas, payload := extractClaudeBetasFromPayload(payload)
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err)
}
attempts, err := p.base.authAttempts(ctx)
if err != nil {
return nil, 0, "", err
}
var lastBody []byte
var lastStatus int
var lastType string
for _, attempt := range attempts {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpointFor(p.baseURL(), "/v1/messages/count_tokens"), bytes.NewReader(jsonData))
if err != nil {
return nil, 0, "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
applyAttemptAuth(req, attempt)
applyAttemptProviderHeaders(req, attempt, p.base, false)
applyClaudeCompatHeaders(req, attempt, false)
applyClaudeBetaHeaders(req, options, extraBetas)
body, status, ctype, err := p.doJSONAttempt(req, attempt)
if err != nil {
return nil, 0, "", err
}
reason, retry := classifyOAuthFailure(status, body)
if !retry {
p.base.markAttemptSuccess(attempt)
return body, status, ctype, nil
}
lastBody, lastStatus, lastType = body, status, ctype
if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil {
p.base.oauth.markExhausted(attempt.session, reason)
recordProviderOAuthError(p.base.providerName, attempt.session, reason)
}
if attempt.kind == "api_key" {
p.base.markAPIKeyFailure(reason)
}
}
return lastBody, lastStatus, lastType, nil
}
func (p *ClaudeProvider) consumeClaudeStream(req *http.Request, attempt authAttempt, onDelta func(string)) ([]byte, int, string, bool, error) {
client, err := p.base.httpClientForAttempt(attempt)
if err != nil {
return nil, 0, "", false, err
}
resp, err := client.Do(req)
if err != nil {
return nil, 0, "", false, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
ctype := strings.TrimSpace(resp.Header.Get("Content-Type"))
if !strings.Contains(strings.ToLower(ctype), "text/event-stream") {
body, readErr := readClaudeBody(resp.Body, resp.Header.Get("Content-Encoding"))
if readErr != nil {
return nil, resp.StatusCode, ctype, false, fmt.Errorf("failed to read response: %w", readErr)
}
return body, resp.StatusCode, ctype, shouldRetryOAuthQuota(resp.StatusCode, body), nil
}
decodedBody, err := decodeClaudeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
if err != nil {
return nil, resp.StatusCode, ctype, false, err
}
defer decodedBody.Close()
scanner := bufio.NewScanner(decodedBody)
scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
var dataLines []string
state := &claudeStreamState{}
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
if len(dataLines) > 0 {
payload := strings.Join(dataLines, "\n")
dataLines = dataLines[:0]
if strings.TrimSpace(payload) != "" && strings.TrimSpace(payload) != "[DONE]" {
if delta := state.consume(stripClaudeToolPrefixFromStreamLine([]byte(payload), claudeToolPrefix)); delta != "" {
onDelta(delta)
}
}
}
continue
}
if strings.HasPrefix(line, "data:") {
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
}
}
if err := scanner.Err(); err != nil {
return nil, resp.StatusCode, ctype, false, fmt.Errorf("failed to read stream: %w", err)
}
return state.finalBody(), resp.StatusCode, ctype, false, nil
}
type claudeStreamState struct {
blocks map[int]*claudeStreamBlock
order []int
Usage *UsageInfo
FinishReason string
}
type claudeStreamBlock struct {
Index int
Type string
Text string
Tool *ToolCall
ArgsRaw string
Finalized bool
}
func (s *claudeStreamState) consume(payload []byte) string {
s.ensureInit()
var event map[string]interface{}
if err := json.Unmarshal(payload, &event); err != nil {
return ""
}
switch strings.TrimSpace(asString(event["type"])) {
case "message_start":
message := mapFromAny(event["message"])
usage := mapFromAny(message["usage"])
if len(usage) > 0 {
s.mergeUsage(usage)
}
if content, ok := claudeMapSlice(message["content"]); ok {
for idx, item := range content {
switch strings.ToLower(strings.TrimSpace(asString(item["type"]))) {
case "text":
s.mergeText(idx, asString(item["text"]), false)
case "tool_use":
name := asString(item["name"])
args := mapFromAny(item["input"])
raw, _ := json.Marshal(args)
block := s.blockAt(idx, "tool_use")
block.Tool = &ToolCall{
ID: asString(item["id"]),
Name: name,
Arguments: args,
Function: &FunctionCall{
Name: name,
Arguments: string(raw),
},
}
block.ArgsRaw = string(raw)
block.Finalized = true
}
}
}
case "content_block_start":
content := mapFromAny(event["content_block"])
index := intValue(event["index"])
switch strings.TrimSpace(asString(content["type"])) {
case "text":
return s.mergeText(index, asString(content["text"]), false)
case "tool_use":
block := s.blockAt(index, "tool_use")
if block.Tool == nil {
block.Tool = &ToolCall{
ID: asString(content["id"]),
Name: asString(content["name"]),
Function: &FunctionCall{
Name: asString(content["name"]),
},
}
} else {
if block.Tool.ID == "" {
block.Tool.ID = asString(content["id"])
}
if block.Tool.Name == "" {
block.Tool.Name = asString(content["name"])
}
if block.Tool.Function == nil {
block.Tool.Function = &FunctionCall{}
}
if block.Tool.Function.Name == "" {
block.Tool.Function.Name = firstNonEmpty(asString(content["name"]), block.Tool.Name)
}
}
input := mapFromAny(content["input"])
if len(input) > 0 {
raw, _ := json.Marshal(input)
if len(raw) > 0 && raw[len(raw)-1] == '}' {
raw = raw[:len(raw)-1]
}
block.ArgsRaw = string(raw)
block.Finalized = false
} else if block.ArgsRaw == "" && !block.Finalized {
block.ArgsRaw = ""
}
}
case "content_block_delta":
delta := mapFromAny(event["delta"])
index := intValue(event["index"])
switch strings.TrimSpace(asString(delta["type"])) {
case "text_delta":
return s.mergeText(index, asString(delta["text"]), true)
case "input_json_delta":
block := s.blockAt(index, "tool_use")
if block.Tool != nil {
block.ArgsRaw += asString(delta["partial_json"])
}
}
case "content_block_stop":
index := intValue(event["index"])
block := s.blockAt(index, "tool_use")
if block.Tool != nil && !block.Finalized {
argsRaw := strings.TrimSpace(block.ArgsRaw)
if argsRaw != "" && !strings.HasSuffix(argsRaw, "}") {
argsRaw += "}"
}
args := map[string]interface{}{}
if argsRaw != "" {
_ = json.Unmarshal([]byte(argsRaw), &args)
}
block.Tool.Function.Arguments = argsRaw
block.Tool.Arguments = args
block.ArgsRaw = argsRaw
block.Finalized = true
}
case "message_delta":
delta := mapFromAny(event["delta"])
s.FinishReason = strings.TrimSpace(firstNonEmpty(asString(delta["stop_reason"]), s.FinishReason))
usage := mapFromAny(event["usage"])
if len(usage) > 0 {
s.mergeUsage(usage)
}
case "message_stop":
if s.FinishReason == "" {
s.FinishReason = "stop"
}
}
return ""
}
func (s *claudeStreamState) ensureInit() {
if s.blocks == nil {
s.blocks = map[int]*claudeStreamBlock{}
}
}
func (s *claudeStreamState) blockAt(index int, typ string) *claudeStreamBlock {
s.ensureInit()
block, ok := s.blocks[index]
if !ok {
block = &claudeStreamBlock{Index: index, Type: typ}
s.blocks[index] = block
s.order = append(s.order, index)
} else if block.Type == "" {
block.Type = typ
}
return block
}
func (s *claudeStreamState) mergeText(index int, incoming string, isDelta bool) string {
incoming = asString(incoming)
if incoming == "" {
return ""
}
block := s.blockAt(index, "text")
if block.Type == "" {
block.Type = "text"
}
if isDelta {
if strings.HasSuffix(block.Text, incoming) {
return ""
}
block.Text += incoming
return incoming
}
if block.Text == "" {
block.Text = incoming
return incoming
}
if strings.HasPrefix(block.Text, incoming) {
return ""
}
if strings.HasPrefix(incoming, block.Text) {
delta := incoming[len(block.Text):]
block.Text = incoming
return delta
}
block.Text += incoming
return incoming
}
func (s *claudeStreamState) mergeUsage(usage map[string]interface{}) {
if len(usage) == 0 {
return
}
if s.Usage == nil {
s.Usage = &UsageInfo{}
}
if v := intValue(usage["input_tokens"]); v > 0 {
s.Usage.PromptTokens = v
}
if v := intValue(usage["output_tokens"]); v > 0 {
s.Usage.CompletionTokens = v
}
s.Usage.TotalTokens = s.Usage.PromptTokens + s.Usage.CompletionTokens
}
func (s *claudeStreamState) finalBody() []byte {
s.ensureInit()
content := make([]map[string]interface{}, 0, len(s.blocks))
order := append([]int(nil), s.order...)
sort.Ints(order)
for _, index := range order {
block := s.blocks[index]
if block == nil {
continue
}
switch block.Type {
case "text":
if txt := strings.TrimSpace(block.Text); txt != "" {
content = append(content, map[string]interface{}{"type": "text", "text": txt})
}
case "tool_use":
if block.Tool == nil {
continue
}
input := block.Tool.Arguments
if len(input) == 0 && block.Tool.Function != nil && strings.TrimSpace(block.Tool.Function.Arguments) != "" {
_ = json.Unmarshal([]byte(block.Tool.Function.Arguments), &input)
}
content = append(content, map[string]interface{}{
"type": "tool_use",
"id": block.Tool.ID,
"name": block.Tool.Name,
"input": input,
})
}
}
body := map[string]interface{}{
"content": content,
"stop_reason": firstNonEmpty(strings.TrimSpace(s.FinishReason), "stop"),
}
if s.Usage != nil {
body["usage"] = map[string]interface{}{
"input_tokens": s.Usage.PromptTokens,
"output_tokens": s.Usage.CompletionTokens,
}
}
raw, _ := json.Marshal(body)
return raw
}
func parseClaudeResponse(body []byte) (*LLMResponse, error) {
var payload struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input map[string]interface{} `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
resp := &LLMResponse{
FinishReason: firstNonEmpty(strings.TrimSpace(payload.StopReason), "stop"),
}
texts := make([]string, 0)
for _, item := range payload.Content {
switch strings.TrimSpace(item.Type) {
case "text":
if strings.TrimSpace(item.Text) != "" {
texts = append(texts, item.Text)
}
case "tool_use":
raw, _ := json.Marshal(item.Input)
resp.ToolCalls = append(resp.ToolCalls, ToolCall{
ID: item.ID,
Name: item.Name,
Arguments: item.Input,
Function: &FunctionCall{
Name: item.Name,
Arguments: string(raw),
},
})
}
}
resp.Content = strings.TrimSpace(strings.Join(texts, "\n"))
total := payload.Usage.InputTokens + payload.Usage.OutputTokens
if total > 0 {
resp.Usage = &UsageInfo{
PromptTokens: payload.Usage.InputTokens,
CompletionTokens: payload.Usage.OutputTokens,
TotalTokens: total,
}
}
return resp, nil
}
func (p *ClaudeProvider) doJSONAttempt(req *http.Request, attempt authAttempt) ([]byte, int, string, error) {
client, err := p.base.httpClientForAttempt(attempt)
if err != nil {
return nil, 0, "", err
}
resp, err := client.Do(req)
if err != nil {
return nil, 0, "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, readErr := readClaudeBody(resp.Body, resp.Header.Get("Content-Encoding"))
if readErr != nil {
return nil, resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), fmt.Errorf("failed to read response: %w", readErr)
}
return body, resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), nil
}
func claudeToolChoice(options map[string]interface{}) map[string]interface{} {
raw, ok := rawOption(options, "tool_choice")
if !ok {
return nil
}
switch typed := raw.(type) {
case string:
val := strings.TrimSpace(strings.ToLower(typed))
switch val {
case "auto", "any":
return map[string]interface{}{"type": val}
case "none":
return nil
case "required":
return map[string]interface{}{"type": "any"}
default:
if val != "" {
return map[string]interface{}{"type": "tool", "name": typed}
}
}
case map[string]interface{}:
switch strings.ToLower(strings.TrimSpace(asString(typed["type"]))) {
case "none":
return nil
case "required":
return map[string]interface{}{"type": "any"}
case "auto", "any":
return map[string]interface{}{"type": strings.ToLower(strings.TrimSpace(asString(typed["type"])))}
case "function":
function := mapFromAny(typed["function"])
name := strings.TrimSpace(asString(function["name"]))
if name != "" {
return map[string]interface{}{"type": "tool", "name": name}
}
}
return typed
}
return nil
}
func disableClaudeThinkingIfToolChoiceForced(body map[string]interface{}) map[string]interface{} {
if body == nil {
return nil
}
toolChoice := mapFromAny(body["tool_choice"])
switch strings.TrimSpace(strings.ToLower(asString(toolChoice["type"]))) {
case "any", "tool":
delete(body, "thinking")
if outputConfig := mapFromAny(body["output_config"]); len(outputConfig) > 0 {
delete(outputConfig, "effort")
if len(outputConfig) == 0 {
delete(body, "output_config")
} else {
body["output_config"] = outputConfig
}
}
}
return body
}
func applyClaudeCompatHeaders(req *http.Request, attempt authAttempt, stream bool) {
if req == nil {
return
}
req.Header.Set("Anthropic-Version", "2023-06-01")
req.Header.Set("Anthropic-Dangerous-Direct-Browser-Access", "true")
req.Header.Set("X-App", "cli")
req.Header.Set("X-Stainless-Retry-Count", "0")
req.Header.Set("X-Stainless-Runtime-Version", "v24.3.0")
req.Header.Set("X-Stainless-Package-Version", "0.74.0")
req.Header.Set("X-Stainless-Runtime", "node")
req.Header.Set("X-Stainless-Lang", "js")
req.Header.Set("X-Stainless-Arch", claudeStainlessArch())
req.Header.Set("X-Stainless-Os", claudeStainlessOS())
req.Header.Set("X-Stainless-Timeout", "600")
req.Header.Set("User-Agent", "claude-cli/2.1.63 (external, cli)")
req.Header.Set("Connection", "keep-alive")
if stream {
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Accept-Encoding", "identity")
} else {
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
}
// Anthropic native base should use x-api-key for api_key mode and Bearer for OAuth.
if attempt.kind == "api_key" && req.URL != nil && strings.EqualFold(req.URL.Host, "api.anthropic.com") {
req.Header.Del("Authorization")
req.Header.Set("x-api-key", strings.TrimSpace(attempt.token))
} else {
req.Header.Del("x-api-key")
if strings.TrimSpace(attempt.token) != "" {
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(attempt.token))
}
}
}
func claudeStainlessOS() string {
switch runtime.GOOS {
case "darwin":
return "MacOS"
case "windows":
return "Windows"
case "linux":
return "Linux"
case "freebsd":
return "FreeBSD"
default:
return "Other::" + runtime.GOOS
}
}
func claudeStainlessArch() string {
switch runtime.GOARCH {
case "amd64":
return "x64"
case "arm64":
return "arm64"
case "386":
return "x86"
default:
return "other::" + runtime.GOARCH
}
}
func applyClaudeBetaHeaders(req *http.Request, options map[string]interface{}, extraBetas []string) {
if req == nil {
return
}
base := strings.TrimSpace(req.Header.Get("Anthropic-Beta"))
if base == "" {
base = "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
}
seen := map[string]bool{}
out := make([]string, 0)
for _, item := range strings.Split(base, ",") {
beta := strings.TrimSpace(item)
if beta == "" || seen[beta] {
continue
}
seen[beta] = true
out = append(out, beta)
}
for _, key := range []string{"claude_betas", "betas"} {
values, ok := stringSliceOption(options, key)
if !ok {
continue
}
for _, beta := range values {
beta = strings.TrimSpace(beta)
if beta == "" || seen[beta] {
continue
}
seen[beta] = true
out = append(out, beta)
}
}
for _, beta := range extraBetas {
beta = strings.TrimSpace(beta)
if beta == "" || seen[beta] {
continue
}
seen[beta] = true
out = append(out, beta)
}
if claudeContext1MEnabled(options) && !seen["context-1m-2025-08-07"] {
out = append(out, "context-1m-2025-08-07")
}
req.Header.Set("Anthropic-Beta", strings.Join(out, ","))
}
func claudeStrictSystemEnabled(options map[string]interface{}) bool {
return claudeBoolOption(options, "claude_strict_system") || claudeBoolOption(options, "cloak_strict_mode")
}
func claudeContext1MEnabled(options map[string]interface{}) bool {
return claudeBoolOption(options, "claude_1m") || claudeBoolOption(options, "context_1m")
}
func claudeBoolOption(options map[string]interface{}, key string) bool {
if len(options) == 0 {
return false
}
raw, ok := options[key]
if !ok {
return false
}
switch typed := raw.(type) {
case bool:
return typed
case string:
switch strings.ToLower(strings.TrimSpace(typed)) {
case "1", "true", "yes", "on":
return true
}
case int:
return typed != 0
case int64:
return typed != 0
case float64:
return typed != 0
}
return false
}
type claudeCompositeReadCloser struct {
io.Reader
closers []func() error
}
func (c *claudeCompositeReadCloser) Close() error {
var firstErr error
for _, closer := range c.closers {
if closer == nil {
continue
}
if err := closer(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
type claudePeekableBody struct {
*bufio.Reader
closer io.Closer
}
func (p *claudePeekableBody) Close() error {
return p.closer.Close()
}
func decodeClaudeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadCloser, error) {
if body == nil {
return nil, fmt.Errorf("response body is nil")
}
if strings.TrimSpace(contentEncoding) == "" {
pb := &claudePeekableBody{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:
gz, err := gzip.NewReader(pb)
if err != nil {
_ = pb.Close()
return nil, err
}
return &claudeCompositeReadCloser{Reader: gz, closers: []func() error{gz.Close, pb.Close}}, nil
case len(magic) >= 4 && magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd:
decoder, err := zstd.NewReader(pb)
if err != nil {
_ = pb.Close()
return nil, err
}
return &claudeCompositeReadCloser{Reader: decoder, closers: []func() error{func() error { decoder.Close(); return nil }, pb.Close}}, nil
}
}
return pb, nil
}
for _, raw := range strings.Split(contentEncoding, ",") {
switch strings.TrimSpace(strings.ToLower(raw)) {
case "", "identity":
continue
case "gzip":
gz, err := gzip.NewReader(body)
if err != nil {
_ = body.Close()
return nil, err
}
return &claudeCompositeReadCloser{Reader: gz, closers: []func() error{gz.Close, body.Close}}, nil
case "deflate":
reader := flate.NewReader(body)
return &claudeCompositeReadCloser{Reader: reader, closers: []func() error{reader.Close, body.Close}}, nil
case "br":
return &claudeCompositeReadCloser{Reader: brotli.NewReader(body), closers: []func() error{body.Close}}, nil
case "zstd":
decoder, err := zstd.NewReader(body)
if err != nil {
_ = body.Close()
return nil, err
}
return &claudeCompositeReadCloser{Reader: decoder, closers: []func() error{func() error { decoder.Close(); return nil }, body.Close}}, nil
}
}
return body, nil
}
func readClaudeBody(body io.ReadCloser, contentEncoding string) ([]byte, error) {
decoded, err := decodeClaudeResponseBody(body, contentEncoding)
if err != nil {
return nil, err
}
defer decoded.Close()
return io.ReadAll(decoded)
}
func ensureClaudeCacheControl(body map[string]interface{}) map[string]interface{} {
if body == nil {
return nil
}
injectClaudeToolsCacheControl(body)
injectClaudeSystemCacheControl(body)
injectClaudeMessagesCacheControl(body)
return body
}
func injectClaudeToolsCacheControl(body map[string]interface{}) {
tools, ok := claudeMapSlice(body["tools"])
if !ok || len(tools) == 0 {
return
}
for _, tool := range tools {
if _, exists := tool["cache_control"]; exists {
body["tools"] = tools
return
}
}
tools[len(tools)-1]["cache_control"] = map[string]interface{}{"type": "ephemeral"}
body["tools"] = tools
}
func injectClaudeSystemCacheControl(body map[string]interface{}) {
switch typed := body["system"].(type) {
case string:
text := strings.TrimSpace(typed)
if text == "" {
return
}
body["system"] = []map[string]interface{}{{
"type": "text",
"text": text,
"cache_control": map[string]interface{}{"type": "ephemeral"},
}}
case []map[string]interface{}:
for _, item := range typed {
if _, exists := item["cache_control"]; exists {
return
}
}
if len(typed) > 0 {
typed[len(typed)-1]["cache_control"] = map[string]interface{}{"type": "ephemeral"}
body["system"] = typed
}
case []interface{}:
if items, ok := claudeMapSlice(typed); ok && len(items) > 0 {
for _, item := range items {
if _, exists := item["cache_control"]; exists {
body["system"] = items
return
}
}
items[len(items)-1]["cache_control"] = map[string]interface{}{"type": "ephemeral"}
body["system"] = items
}
}
}
func injectClaudeMessagesCacheControl(body map[string]interface{}) {
messages, ok := claudeMapSlice(body["messages"])
if !ok || len(messages) == 0 {
return
}
for _, msg := range messages {
content, ok := claudeMapSlice(msg["content"])
if !ok {
continue
}
for _, item := range content {
if _, exists := item["cache_control"]; exists {
body["messages"] = messages
return
}
}
}
userIdx := make([]int, 0)
for idx, msg := range messages {
if strings.EqualFold(asString(msg["role"]), "user") {
userIdx = append(userIdx, idx)
}
}
if len(userIdx) < 2 {
body["messages"] = messages
return
}
target := messages[userIdx[len(userIdx)-2]]
content, ok := claudeMapSlice(target["content"])
if ok && len(content) > 0 {
content[len(content)-1]["cache_control"] = map[string]interface{}{"type": "ephemeral"}
target["content"] = content
}
body["messages"] = messages
}
func enforceClaudeCacheControlLimit(body map[string]interface{}, maxBlocks int) map[string]interface{} {
if body == nil || maxBlocks <= 0 {
return body
}
blocks := claudeCacheBlocks(body)
if len(blocks) <= maxBlocks {
return body
}
excess := len(blocks) - maxBlocks
system, _ := claudeMapSlice(body["system"])
tools, _ := claudeMapSlice(body["tools"])
messages, _ := claudeMapSlice(body["messages"])
excess = stripClaudeCacheControlExceptLast(system, excess)
excess = stripClaudeCacheControlExceptLast(tools, excess)
excess = stripClaudeMessageCacheControl(messages, excess)
excess = stripClaudeAllCacheControl(system, excess)
excess = stripClaudeAllCacheControl(tools, excess)
return body
}
func normalizeClaudeCacheControlTTL(body map[string]interface{}) map[string]interface{} {
if body == nil {
return nil
}
seenDefaultTTL := false
for _, item := range claudeCacheBlocks(body) {
cc := mapFromAny(item["cache_control"])
if strings.TrimSpace(asString(cc["ttl"])) == "1h" {
if seenDefaultTTL {
delete(cc, "ttl")
item["cache_control"] = cc
}
continue
}
seenDefaultTTL = true
}
return body
}
func claudeCacheBlocks(body map[string]interface{}) []map[string]interface{} {
out := make([]map[string]interface{}, 0)
if tools, ok := claudeMapSlice(body["tools"]); ok {
for _, item := range tools {
if _, exists := item["cache_control"]; exists {
out = append(out, item)
}
}
}
switch typed := body["system"].(type) {
case []map[string]interface{}:
for _, item := range typed {
if _, exists := item["cache_control"]; exists {
out = append(out, item)
}
}
case []interface{}:
if items, ok := claudeMapSlice(typed); ok {
for _, item := range items {
if _, exists := item["cache_control"]; exists {
out = append(out, item)
}
}
}
}
if messages, ok := claudeMapSlice(body["messages"]); ok {
for _, msg := range messages {
if content, ok := claudeMapSlice(msg["content"]); ok {
for _, item := range content {
if _, exists := item["cache_control"]; exists {
out = append(out, item)
}
}
}
}
}
return out
}
func applyClaudeToolPrefixToBody(body map[string]interface{}, prefix string) map[string]interface{} {
if prefix == "" || body == nil {
return body
}
builtinTools := map[string]bool{
"web_search": true,
"code_execution": true,
"text_editor": true,
"computer": true,
}
if tools, ok := claudeMapSlice(body["tools"]); ok {
for _, tool := range tools {
name := strings.TrimSpace(asString(tool["name"]))
if typ := strings.TrimSpace(asString(tool["type"])); typ != "" {
if name != "" {
builtinTools[name] = true
}
continue
}
if name != "" && !strings.HasPrefix(name, prefix) {
tool["name"] = prefix + name
}
}
body["tools"] = tools
}
if toolChoice := mapFromAny(body["tool_choice"]); strings.EqualFold(asString(toolChoice["type"]), "tool") {
name := strings.TrimSpace(asString(toolChoice["name"]))
if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] {
toolChoice["name"] = prefix + name
body["tool_choice"] = toolChoice
}
}
if messages, ok := claudeMapSlice(body["messages"]); ok {
for _, msg := range messages {
if content, ok := claudeMapSlice(msg["content"]); ok {
for _, item := range content {
switch strings.ToLower(strings.TrimSpace(asString(item["type"]))) {
case "tool_use":
name := strings.TrimSpace(asString(item["name"]))
if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] {
item["name"] = prefix + name
}
case "tool_reference":
name := strings.TrimSpace(asString(item["tool_name"]))
if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] {
item["tool_name"] = prefix + name
}
case "tool_result":
if nested, ok := claudeMapSlice(item["content"]); ok {
for _, nestedItem := range nested {
if strings.EqualFold(asString(nestedItem["type"]), "tool_reference") {
name := strings.TrimSpace(asString(nestedItem["tool_name"]))
if name != "" && !strings.HasPrefix(name, prefix) && !builtinTools[name] {
nestedItem["tool_name"] = prefix + name
}
}
}
item["content"] = nested
}
}
}
msg["content"] = content
}
}
body["messages"] = messages
}
return body
}
func stripClaudeToolPrefixFromResponse(body []byte, prefix string) []byte {
if prefix == "" || len(body) == 0 {
return body
}
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
return body
}
if content, ok := claudeMapSlice(payload["content"]); ok {
for _, item := range content {
switch strings.ToLower(strings.TrimSpace(asString(item["type"]))) {
case "tool_use":
name := strings.TrimSpace(asString(item["name"]))
if strings.HasPrefix(name, prefix) {
item["name"] = strings.TrimPrefix(name, prefix)
}
case "tool_reference":
name := strings.TrimSpace(asString(item["tool_name"]))
if strings.HasPrefix(name, prefix) {
item["tool_name"] = strings.TrimPrefix(name, prefix)
}
case "tool_result":
if nested, ok := claudeMapSlice(item["content"]); ok {
for _, nestedItem := range nested {
if strings.EqualFold(asString(nestedItem["type"]), "tool_reference") {
name := strings.TrimSpace(asString(nestedItem["tool_name"]))
if strings.HasPrefix(name, prefix) {
nestedItem["tool_name"] = strings.TrimPrefix(name, prefix)
}
}
}
item["content"] = nested
}
}
}
payload["content"] = content
}
updated, err := json.Marshal(payload)
if err != nil {
return body
}
return updated
}
func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte {
if prefix == "" || len(line) == 0 {
return line
}
trimmed := bytes.TrimSpace(line)
hasDataPrefix := bytes.HasPrefix(trimmed, []byte("data:"))
payloadBytes := trimmed
if hasDataPrefix {
payloadBytes = bytes.TrimSpace(bytes.TrimPrefix(trimmed, []byte("data:")))
}
var payload map[string]interface{}
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
return line
}
contentBlock := mapFromAny(payload["content_block"])
switch strings.ToLower(strings.TrimSpace(asString(contentBlock["type"]))) {
case "tool_use":
name := strings.TrimSpace(asString(contentBlock["name"]))
if strings.HasPrefix(name, prefix) {
contentBlock["name"] = strings.TrimPrefix(name, prefix)
payload["content_block"] = contentBlock
}
case "tool_reference":
name := strings.TrimSpace(asString(contentBlock["tool_name"]))
if strings.HasPrefix(name, prefix) {
contentBlock["tool_name"] = strings.TrimPrefix(name, prefix)
payload["content_block"] = contentBlock
}
}
updated, err := json.Marshal(payload)
if err != nil {
return line
}
if hasDataPrefix {
return append([]byte("data: "), updated...)
}
return updated
}
func claudeMapSlice(value interface{}) ([]map[string]interface{}, bool) {
switch typed := value.(type) {
case []map[string]interface{}:
return typed, true
case []interface{}:
out := make([]map[string]interface{}, 0, len(typed))
for _, item := range typed {
obj := mapFromAny(item)
if len(obj) > 0 {
out = append(out, obj)
}
}
return out, len(out) > 0
default:
return nil, false
}
}
func extractClaudeBetasFromPayload(payload map[string]interface{}) ([]string, map[string]interface{}) {
if payload == nil {
return nil, nil
}
out := make([]string, 0)
for _, key := range []string{"betas", "claude_betas"} {
values, ok := stringSliceOption(payload, key)
if ok {
out = append(out, values...)
delete(payload, key)
continue
}
if raw, exists := payload[key]; exists {
if beta := strings.TrimSpace(asString(raw)); beta != "" {
out = append(out, beta)
}
delete(payload, key)
}
}
return out, payload
}
func stripClaudeCacheControlExceptLast(items []map[string]interface{}, excess int) int {
if excess <= 0 || len(items) == 0 {
return excess
}
last := -1
for idx := len(items) - 1; idx >= 0; idx-- {
if _, exists := items[idx]["cache_control"]; exists {
last = idx
break
}
}
for idx := 0; idx < len(items) && excess > 0; idx++ {
if idx == last {
continue
}
if _, exists := items[idx]["cache_control"]; exists {
delete(items[idx], "cache_control")
excess--
}
}
return excess
}
func stripClaudeAllCacheControl(items []map[string]interface{}, excess int) int {
if excess <= 0 {
return excess
}
for _, item := range items {
if excess <= 0 {
return excess
}
if _, exists := item["cache_control"]; exists {
delete(item, "cache_control")
excess--
}
}
return excess
}
func stripClaudeMessageCacheControl(messages []map[string]interface{}, excess int) int {
if excess <= 0 {
return excess
}
for _, msg := range messages {
content, ok := claudeMapSlice(msg["content"])
if !ok {
continue
}
for _, item := range content {
if excess <= 0 {
return excess
}
if _, exists := item["cache_control"]; exists {
delete(item, "cache_control")
excess--
}
}
msg["content"] = content
}
return excess
}