Skip to content

Commit 71e742a

Browse files
committed
feat: breaking changes with llms.CallOptions and improve test coverage
- Removed deprecated RedactedThinkingContent from search index and adjusted related tests to enhance clarity. - Updated CountTokens test to reflect correct expected token count. - Refactored marshaling tests to streamline reasoning content handling, removing unnecessary redacted content checks. - Modified CallOptions struct to use pointers for optional fields, improving memory efficiency and consistency. - Enhanced test cases for CallOptions to validate new pointer-based logic and default values. - Updated Anthropic client to utilize new CallOptions methods for better encapsulation and maintainability.
1 parent 274ec52 commit 71e742a

89 files changed

Lines changed: 2631 additions & 2480 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/static/search-index.json

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11625,35 +11625,6 @@
1162511625
"anthropicclient"
1162611626
]
1162711627
},
11628-
{
11629-
"title": "RedactedThinkingContent",
11630-
"url": "https://pkg.go.dev/github.com/vxcontrol/langchaingo/llms/anthropic/internal/anthropicclient#RedactedThinkingContent",
11631-
"content": "",
11632-
"type": "type",
11633-
"package": "anthropicclient",
11634-
"signature": "type RedactedThinkingContent",
11635-
"external": true,
11636-
"keywords": [
11637-
"type",
11638-
"RedactedThinkingContent",
11639-
"anthropicclient"
11640-
]
11641-
},
11642-
{
11643-
"title": "RedactedThinkingContent.GetType",
11644-
"url": "https://pkg.go.dev/github.com/vxcontrol/langchaingo/llms/anthropic/internal/anthropicclient#RedactedThinkingContent.GetType",
11645-
"content": "",
11646-
"type": "method",
11647-
"package": "anthropicclient",
11648-
"signature": "GetType()",
11649-
"external": true,
11650-
"keywords": [
11651-
"method",
11652-
"GetType",
11653-
"RedactedThinkingContent",
11654-
"anthropicclient"
11655-
]
11656-
},
1165711628
{
1165811629
"title": "TextContent",
1165911630
"url": "https://pkg.go.dev/github.com/vxcontrol/langchaingo/llms/anthropic/internal/anthropicclient#TextContent",

llms/anthropic/anthropic_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ func TestAnthropic_GenerateContentWithToolAndStreaming(t *testing.T) {
272272
content,
273273
llms.WithStreamingFunc(streamingFunc),
274274
llms.WithTools(availableTools),
275+
llms.WithTemperature(0.5),
275276
)
276277
require.NoError(t, err)
277278

llms/anthropic/anthropic_thinking_test.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -434,66 +434,3 @@ func TestAnthropic_InterleavedThinkingMultipleBlocks(t *testing.T) {
434434
}
435435
}
436436
}
437-
438-
// TestAnthropic_RedactedThinking tests redacted thinking handling
439-
func TestAnthropic_RedactedThinking(t *testing.T) {
440-
t.Parallel()
441-
442-
llm := newHTTPRRClient(t, anthropic.WithModel("claude-3-7-sonnet-latest"))
443-
444-
// Use magic string to trigger redacted thinking
445-
magicString := "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB"
446-
447-
messages := []llms.MessageContent{
448-
{
449-
Role: llms.ChatMessageTypeHuman,
450-
Parts: []llms.ContentPart{
451-
llms.TextPart(magicString),
452-
},
453-
},
454-
}
455-
456-
// Request with thinking enabled
457-
resp, err := llm.GenerateContent(t.Context(), messages,
458-
llms.WithReasoning(llms.ReasoningHigh, 10000),
459-
llms.WithMaxTokens(16000),
460-
llms.WithTemperature(1.0),
461-
)
462-
require.NoError(t, err)
463-
464-
choice := resp.Choices[0]
465-
assert.NotNil(t, choice.Reasoning)
466-
467-
// Check if redacted content is present
468-
if choice.Reasoning != nil && len(choice.Reasoning.RedactedContent) > 0 {
469-
t.Logf("Found redacted thinking content: %d bytes", len(choice.Reasoning.RedactedContent))
470-
assert.NotEmpty(t, choice.Reasoning.RedactedContent)
471-
472-
// ROUNDTRIP: Send response back with redacted content preserved
473-
messages = append(messages,
474-
llms.MessageContent{
475-
Role: llms.ChatMessageTypeAI,
476-
Parts: []llms.ContentPart{
477-
llms.TextPartWithReasoning(choice.Content, choice.Reasoning),
478-
},
479-
},
480-
llms.MessageContent{
481-
Role: llms.ChatMessageTypeHuman,
482-
Parts: []llms.ContentPart{
483-
llms.TextPart("Continue the conversation"),
484-
},
485-
},
486-
)
487-
488-
// This should work - API should decrypt and use redacted content
489-
resp2, err := llm.GenerateContent(t.Context(), messages,
490-
llms.WithReasoning(llms.ReasoningHigh, 10000),
491-
llms.WithMaxTokens(16000),
492-
llms.WithTemperature(1.0),
493-
)
494-
require.NoError(t, err, "API should accept redacted thinking in roundtrip")
495-
assert.NotEmpty(t, resp2.Choices[0].Content)
496-
} else {
497-
assert.Fail(t, "No redacted thinking in response")
498-
}
499-
}

llms/anthropic/anthropicllm.go

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ func generateCompletionsContent(ctx context.Context, o *LLM, messages []llms.Mes
130130
}
131131
prompt := fmt.Sprintf("\n\nHuman: %s\n\nAssistant:", partText.Text)
132132
result, err := o.client.CreateCompletion(ctx, &anthropicclient.CompletionRequest{
133-
Model: opts.Model,
133+
Model: opts.GetModel(),
134134
Prompt: prompt,
135-
MaxTokens: opts.MaxTokens,
136-
StopWords: opts.StopWords,
137-
Temperature: opts.Temperature,
138-
TopP: opts.TopP,
135+
MaxTokens: opts.GetMaxTokens(),
136+
StopWords: opts.GetStopWords(),
137+
Temperature: opts.GetTemperature(),
138+
TopP: opts.GetTopP(),
139139
StreamingFunc: opts.StreamingFunc,
140140
})
141141
if err != nil {
@@ -165,7 +165,7 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
165165
if opts.Reasoning.IsEnabled() {
166166
thinking = &anthropicclient.ThinkingPayload{
167167
Type: "enabled",
168-
Budget: opts.Reasoning.GetTokens(opts.MaxTokens),
168+
Budget: opts.Reasoning.GetTokens(opts.GetMaxTokens()),
169169
}
170170
}
171171

@@ -198,16 +198,17 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
198198
}
199199

200200
// Set temperature to 1.0 for thinking models
201-
temperature := opts.Temperature
201+
temperature, maxTokens := opts.Temperature, opts.GetMaxTokens()
202202
if thinking != nil && thinking.Type == "enabled" && thinking.Budget > 0 {
203-
temperature = 1.0
203+
temperature = getFloatPointer(1.0)
204+
maxTokens = max(thinking.Budget*2, maxTokens) // 2x the budget for thinking
204205
}
205206

206207
result, err := o.client.CreateMessage(ctx, &anthropicclient.MessageRequest{
207-
Model: opts.Model,
208+
Model: opts.GetModel(),
208209
Messages: chatMessages,
209210
System: system,
210-
MaxTokens: opts.MaxTokens,
211+
MaxTokens: &maxTokens,
211212
StopWords: opts.StopWords,
212213
Temperature: temperature,
213214
TopP: opts.TopP,
@@ -232,11 +233,10 @@ func processAnthropicResponse(result *anthropicclient.MessageResponsePayload) (*
232233
return nil, ErrEmptyResponse
233234
}
234235

235-
// Extract ALL thinking content, signature, and redacted content
236+
// Extract ALL thinking content, signature
236237
// According to Anthropic docs, there's ONE thinking block per response
237238
var reasoningContent strings.Builder
238239
var signature []byte
239-
var redactedContent []byte
240240

241241
for _, content := range result.Content {
242242
switch cv := content.(type) {
@@ -245,21 +245,15 @@ func processAnthropicResponse(result *anthropicclient.MessageResponsePayload) (*
245245
if len(cv.Signature) > 0 {
246246
signature = []byte(cv.Signature)
247247
}
248-
case *anthropicclient.RedactedThinkingContent:
249-
// Redacted content is encrypted and must be preserved for roundtrip
250-
if len(cv.Data) > 0 {
251-
redactedContent = []byte(cv.Data)
252-
}
253248
}
254249
}
255250

256251
// Create reasoning object
257252
var contentReasoning *reasoning.ContentReasoning
258-
if reasoningContent.Len() > 0 || len(signature) > 0 || len(redactedContent) > 0 {
253+
if reasoningContent.Len() > 0 || len(signature) > 0 {
259254
contentReasoning = &reasoning.ContentReasoning{
260-
Content: reasoningContent.String(),
261-
Signature: signature,
262-
RedactedContent: redactedContent,
255+
Content: reasoningContent.String(),
256+
Signature: signature,
263257
}
264258
}
265259

@@ -694,7 +688,7 @@ func handleAIMessage(msg llms.MessageContent) (anthropicclient.ChatMessage, erro
694688
for _, part := range msg.Parts {
695689
switch p := part.(type) {
696690
case llms.TextContent:
697-
// If reasoning is present, add blocks in order: thinking → redacted_thinking → text
691+
// If reasoning is present, add blocks in order: thinking → text
698692
if p.Reasoning != nil {
699693
// Add thinking block if present
700694
if len(p.Reasoning.Content) > 0 || len(p.Reasoning.Signature) > 0 {
@@ -707,15 +701,6 @@ func handleAIMessage(msg llms.MessageContent) (anthropicclient.ChatMessage, erro
707701
}
708702
message.Content = append(message.Content, thinkingBlock)
709703
}
710-
711-
// Add redacted thinking block if present (after thinking)
712-
if len(p.Reasoning.RedactedContent) > 0 {
713-
redactedBlock := &anthropicclient.RedactedThinkingContent{
714-
Type: "redacted_thinking",
715-
Data: string(p.Reasoning.RedactedContent),
716-
}
717-
message.Content = append(message.Content, redactedBlock)
718-
}
719704
}
720705

721706
// Add text content only if not empty
@@ -807,3 +792,7 @@ func appendIfMissing(slice []string, val string) []string {
807792
}
808793
return append(slice, val)
809794
}
795+
796+
func getFloatPointer(f float64) *float64 {
797+
return &f
798+
}

llms/anthropic/internal/anthropicclient/anthropicclient.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ type MessageRequest struct {
137137
Model string `json:"model"`
138138
Messages []ChatMessage `json:"messages"`
139139
System any `json:"system,omitempty"` // Can be string or []Content for caching
140-
Temperature float64 `json:"temperature"`
141-
MaxTokens int `json:"max_tokens,omitempty"`
142-
TopP float64 `json:"top_p,omitempty"`
140+
Temperature *float64 `json:"temperature,omitempty"`
141+
MaxTokens *int `json:"max_tokens,omitempty"`
142+
TopP *float64 `json:"top_p,omitempty"`
143143
Tools []Tool `json:"tools,omitempty"`
144144
ToolChoice any `json:"tool_choice,omitempty"`
145145
StopWords []string `json:"stop_sequences,omitempty"`

llms/anthropic/internal/anthropicclient/anthropicclient_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func TestClient_CreateMessage(t *testing.T) {
4040
},
4141
},
4242
},
43-
MaxTokens: 100,
43+
MaxTokens: getIntPointer(100),
4444
}
4545

4646
resp, err := client.CreateMessage(ctx, req)
@@ -80,7 +80,7 @@ func TestClient_CreateMessageStream(t *testing.T) {
8080
},
8181
},
8282
},
83-
MaxTokens: 100,
83+
MaxTokens: getIntPointer(100),
8484
Stream: true,
8585
StreamingFunc: func(_ context.Context, chunk streaming.Chunk) error {
8686
switch chunk.Type {
@@ -132,7 +132,7 @@ func TestClient_WithAnthropicBetaHeader(t *testing.T) {
132132
},
133133
},
134134
},
135-
MaxTokens: 100,
135+
MaxTokens: getIntPointer(100),
136136
Tools: []Tool{
137137
{
138138
Name: "get_weather",

llms/anthropic/internal/anthropicclient/messages.go

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ var (
4242
ErrInvalidFieldType = fmt.Errorf("invalid field type")
4343
)
4444

45-
// For correct using thinking and redacted thinking events, use this guide:
45+
// For correct using thinking events, use this guide:
4646
// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking
4747
// In this implementation, we don't send the thinking block to the server.
4848
const (
49-
EventTypeText = "text"
50-
EventTypeToolUse = "tool_use"
51-
EventTypeThinking = "thinking"
52-
EventTypeRedactedThinking = "redacted_thinking"
49+
EventTypeText = "text"
50+
EventTypeToolUse = "tool_use"
51+
EventTypeThinking = "thinking"
5352
)
5453

5554
const (
@@ -73,11 +72,11 @@ type messagePayload struct {
7372
Model string `json:"model"`
7473
Messages []ChatMessage `json:"messages"`
7574
System any `json:"system,omitempty"`
76-
MaxTokens int `json:"max_tokens,omitempty"`
75+
MaxTokens *int `json:"max_tokens,omitempty"`
7776
StopWords []string `json:"stop_sequences,omitempty"`
7877
Stream bool `json:"stream,omitempty"`
79-
Temperature float64 `json:"temperature"`
80-
TopP float64 `json:"top_p,omitempty"`
78+
Temperature *float64 `json:"temperature,omitempty"`
79+
TopP *float64 `json:"top_p,omitempty"`
8180
Tools []Tool `json:"tools,omitempty"`
8281
ToolChoice any `json:"tool_choice,omitempty"`
8382

@@ -151,15 +150,6 @@ func (tc ThinkingContent) GetType() string {
151150
return tc.Type
152151
}
153152

154-
type RedactedThinkingContent struct {
155-
Type string `json:"type"`
156-
Data string `json:"data"`
157-
}
158-
159-
func (rthc RedactedThinkingContent) GetType() string {
160-
return rthc.Type
161-
}
162-
163153
type ToolUseContent struct {
164154
Type string `json:"type"`
165155
ID string `json:"id"`
@@ -278,21 +268,15 @@ func parseContentBlock(raw []byte) (Content, error) {
278268
return nil, err
279269
}
280270
return thc, nil
281-
case EventTypeRedactedThinking:
282-
rthc := &RedactedThinkingContent{}
283-
if err := json.Unmarshal(raw, rthc); err != nil {
284-
return nil, err
285-
}
286-
return rthc, nil
287271
default:
288272
return nil, fmt.Errorf("unknown content type: %s\n%v", typeStruct.Type, string(raw)) //nolint:err113
289273
}
290274
}
291275

292276
func (c *Client) setMessageDefaults(payload *messagePayload) {
293277
// Set defaults
294-
if payload.MaxTokens == 0 {
295-
payload.MaxTokens = 2048
278+
if payload.MaxTokens == nil || *payload.MaxTokens == 0 {
279+
payload.MaxTokens = getIntPointer(2048)
296280
}
297281

298282
if len(payload.StopWords) == 0 {
@@ -566,11 +550,6 @@ func handleContentBlockStartEvent(event map[string]interface{}, response Message
566550
Thinking: getString(cb, "thinking"),
567551
Signature: getString(cb, "signature"),
568552
})
569-
case EventTypeRedactedThinking:
570-
response.Content = append(response.Content, &RedactedThinkingContent{
571-
Type: eventType,
572-
Data: getString(cb, "data"),
573-
})
574553
default:
575554
return response, fmt.Errorf("unknown content block type: %s", eventType)
576555
}
@@ -796,3 +775,7 @@ func getMap(m map[string]interface{}, key string) map[string]interface{} {
796775
}
797776
return value
798777
}
778+
779+
func getIntPointer(i int) *int {
780+
return &i
781+
}

0 commit comments

Comments
 (0)